mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 17:17:55 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di
|
||||
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.deeplink.MultisigDeepLinks
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
|
||||
interface MultisigOperationsFeatureApi {
|
||||
|
||||
val multisigDeepLinks: MultisigDeepLinks
|
||||
|
||||
val multisigOperationDeepLinkConfigurator: MultisigOperationDeepLinkConfigurator
|
||||
|
||||
val multisigCallFormatter: MultisigCallFormatter
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.deeplink.DeepLinkModule
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.di.MultisigCreatedComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.full.di.MultisigOperationFullDetailsComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.di.MultisigOperationDetailsComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.enterCall.di.MultisigOperationEnterCallComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.di.MultisigPendingOperationsComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
MultisigOperationsFeatureDependencies::class,
|
||||
],
|
||||
modules = [
|
||||
MultisigOperationsFeatureModule::class,
|
||||
DeepLinkModule::class
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface MultisigOperationsFeatureComponent : MultisigOperationsFeatureApi {
|
||||
|
||||
fun multisigPendingOperations(): MultisigPendingOperationsComponent.Factory
|
||||
|
||||
fun multisigOperationDetails(): MultisigOperationDetailsComponent.Factory
|
||||
|
||||
fun multisigOperationFullDetails(): MultisigOperationFullDetailsComponent.Factory
|
||||
|
||||
fun multisigOperationEnterCall(): MultisigOperationEnterCallComponent.Factory
|
||||
|
||||
fun multisigCreated(): MultisigCreatedComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: MultisigOperationsRouter,
|
||||
deps: MultisigOperationsFeatureDependencies
|
||||
): MultisigOperationsFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
RuntimeApi::class,
|
||||
DbApi::class,
|
||||
WalletFeatureApi::class,
|
||||
AccountFeatureApi::class,
|
||||
DeepLinkingFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface MultisigOperationsFeatureDependenciesComponent : MultisigOperationsFeatureDependencies
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di
|
||||
|
||||
import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.mixin.copy.CopyTextLauncher
|
||||
import io.novafoundation.nova.common.data.repository.ToggleFeatureRepository
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.DialogMessageManager
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSplitter
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigDetailsRepository
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigOperationLocalCallRepository
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigValidationsRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalWithOnChainIdentity
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.delegeted.MultisigFormatter
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.delegeted.ProxyFormatter
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.runtime.di.ExtrinsicSerialization
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallTraversal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
interface MultisigOperationsFeatureDependencies {
|
||||
|
||||
val tokenFormatter: TokenFormatter
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
val assetIconProvider: AssetIconProvider
|
||||
|
||||
val extrinsicSplitter: ExtrinsicSplitter
|
||||
|
||||
val accountRepository: AccountRepository
|
||||
|
||||
val extrinsicService: ExtrinsicService
|
||||
|
||||
val resourceManager: ResourceManager
|
||||
|
||||
val multisigPendingOperationsService: MultisigPendingOperationsService
|
||||
|
||||
val feeLoaderMixinFactory: FeeLoaderMixinV2.Factory
|
||||
|
||||
val imageLoader: ImageLoader
|
||||
|
||||
val externalActions: ExternalActions.Presentation
|
||||
|
||||
val validationExecutor: ValidationExecutor
|
||||
|
||||
val selectedAccountUseCase: SelectedAccountUseCase
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper
|
||||
|
||||
val edValidationFactory: EnoughTotalToStayAboveEDValidationFactory
|
||||
|
||||
val assetSourceRegistry: AssetSourceRegistry
|
||||
|
||||
val multisigOperationLocalCallRepository: MultisigOperationLocalCallRepository
|
||||
|
||||
val callTraversal: CallTraversal
|
||||
|
||||
val addressIconGenerator: AddressIconGenerator
|
||||
|
||||
val multisigFormatter: MultisigFormatter
|
||||
|
||||
val proxyFormatter: ProxyFormatter
|
||||
|
||||
val accountInteractor: AccountInteractor
|
||||
|
||||
val arbitraryTokenUseCase: ArbitraryTokenUseCase
|
||||
|
||||
val toggleFeatureRepository: ToggleFeatureRepository
|
||||
|
||||
val actionBottomSheetLauncherFactory: ActionBottomSheetLauncherFactory
|
||||
|
||||
val multisigValidationsRepository: MultisigValidationsRepository
|
||||
|
||||
val tokenRepository: TokenRepository
|
||||
|
||||
val chainRegistry: ChainRegistry
|
||||
|
||||
val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher
|
||||
|
||||
val copyTextLauncher: CopyTextLauncher.Presentation
|
||||
|
||||
val accountUIUseCase: AccountUIUseCase
|
||||
|
||||
val linkBuilderFactory: LinkBuilderFactory
|
||||
|
||||
val automaticInteractionGate: AutomaticInteractionGate
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val multisigDetailsRepository: MultisigDetailsRepository
|
||||
|
||||
fun dialogMessageManager(): DialogMessageManager
|
||||
|
||||
@LocalIdentity
|
||||
fun localIdentityProvider(): IdentityProvider
|
||||
|
||||
@LocalWithOnChainIdentity
|
||||
fun localWithOnChainIdentityProvider(): IdentityProvider
|
||||
|
||||
@ExtrinsicSerialization
|
||||
fun extrinsicGson(): Gson
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class MultisigOperationsFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val router: MultisigOperationsRouter,
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val accountFeatureDependencies = DaggerMultisigOperationsFeatureComponent_MultisigOperationsFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.deepLinkingFeatureApi(getFeature(DeepLinkingFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerMultisigOperationsFeatureComponent.factory()
|
||||
.create(
|
||||
router = router,
|
||||
deps = accountFeatureDependencies
|
||||
)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoSet
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.delegeted.MultisigFormatter
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.delegeted.ProxyFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureModule.BindsModule
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters.MultisigActionFormatterDelegate
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters.RealMultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters.TransferMultisigActionFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters.UtilityBatchesActionFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.SignatoryListFormatter
|
||||
|
||||
@Module(includes = [BindsModule::class])
|
||||
class MultisigOperationsFeatureModule {
|
||||
|
||||
@Module
|
||||
internal interface BindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindMultisigCallFormatter(real: RealMultisigCallFormatter): MultisigCallFormatter
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindTransferCallFormatter(real: TransferMultisigActionFormatter): MultisigActionFormatterDelegate
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindUtilityBatchCallFormatter(real: UtilityBatchesActionFormatter): MultisigActionFormatterDelegate
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSignatoryListFormatter(
|
||||
accountInteractor: AccountInteractor,
|
||||
proxyFormatter: ProxyFormatter,
|
||||
multisigFormatter: MultisigFormatter
|
||||
): SignatoryListFormatter {
|
||||
return SignatoryListFormatter(
|
||||
accountInteractor,
|
||||
proxyFormatter,
|
||||
multisigFormatter
|
||||
)
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di.deeplink
|
||||
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.RealMultisigOperationDeepLinkConfigurator
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.DialogMessageManager
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDetailsDeepLinkHandler
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class DeepLinkModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinkConfigurator(
|
||||
linkBuilderFactory: LinkBuilderFactory
|
||||
): MultisigOperationDeepLinkConfigurator {
|
||||
return RealMultisigOperationDeepLinkConfigurator(linkBuilderFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideMultisigOperationDetailsDeepLinkHandler(
|
||||
router: MultisigOperationsRouter,
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
automaticInteractionGate: AutomaticInteractionGate,
|
||||
dialogMessageManager: DialogMessageManager,
|
||||
multisigCallFormatter: MultisigCallFormatter,
|
||||
): MultisigOperationDetailsDeepLinkHandler {
|
||||
return MultisigOperationDetailsDeepLinkHandler(
|
||||
router,
|
||||
accountRepository,
|
||||
chainRegistry,
|
||||
automaticInteractionGate,
|
||||
dialogMessageManager,
|
||||
multisigCallFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinks(operationDeepLink: MultisigOperationDetailsDeepLinkHandler): MultisigDeepLinks {
|
||||
return MultisigDeepLinks(listOf(operationDeepLink))
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.di.deeplink
|
||||
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
|
||||
class MultisigDeepLinks(val deepLinkHandlers: List<DeepLinkHandler>)
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.domain.details
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
|
||||
import io.novafoundation.nova.common.data.repository.ToggleFeatureRepository
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.callHash
|
||||
import io.novafoundation.nova.common.utils.toHex
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSplitter
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigDetailsRepository
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.composeMultisigAsMulti
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.composeMultisigCancelAsMulti
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigOperationLocalCallRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.SavedMultisigOperationCall
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.multisig.intoCallHash
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.queryAccountBalanceCatching
|
||||
import io.novafoundation.nova.runtime.di.ExtrinsicSerialization
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
interface MultisigOperationDetailsInteractor {
|
||||
|
||||
suspend fun setCall(operation: PendingMultisigOperation, call: String)
|
||||
|
||||
fun callDetails(call: GenericCall.Instance): String
|
||||
|
||||
suspend fun callHash(call: GenericCall.Instance, chainId: ChainId): String
|
||||
|
||||
suspend fun estimateActionFee(operation: PendingMultisigOperation): Fee?
|
||||
|
||||
suspend fun performAction(operation: PendingMultisigOperation): Result<ExtrinsicExecutionResult>
|
||||
|
||||
fun signatoryFlow(signatoryMetaId: Long): Flow<MetaAccount>
|
||||
|
||||
suspend fun getSignatoryBalance(signatory: MetaAccount, chain: Chain): Result<ChainAssetBalance>
|
||||
|
||||
fun isCallValid(operation: PendingMultisigOperation, enteredCall: String): Boolean
|
||||
|
||||
fun setSkipRejectConfirmation(value: Boolean)
|
||||
|
||||
fun getSkipRejectConfirmation(): Boolean
|
||||
|
||||
suspend fun callDataAsString(call: GenericCall.Instance, chainId: ChainId): String
|
||||
|
||||
suspend fun isOperationAvailable(operationId: PendingMultisigOperationId): Boolean
|
||||
}
|
||||
|
||||
private const val SKIP_REJECT_CONFIRMATION_KEY = "SKIP_REJECT_CONFIRMATION_KEY"
|
||||
|
||||
@FeatureScope
|
||||
class RealMultisigOperationDetailsInteractor @Inject constructor(
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val extrinsicSplitter: ExtrinsicSplitter,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val multisigOperationLocalCallRepository: MultisigOperationLocalCallRepository,
|
||||
@ExtrinsicSerialization
|
||||
private val extrinsicGson: Gson,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val toggleFeatureRepository: ToggleFeatureRepository,
|
||||
private val multisigDetailsRepository: MultisigDetailsRepository
|
||||
) : MultisigOperationDetailsInteractor {
|
||||
|
||||
override suspend fun setCall(operation: PendingMultisigOperation, call: String) {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
multisigOperationLocalCallRepository.setMultisigCall(
|
||||
SavedMultisigOperationCall(
|
||||
metaId = metaAccount.id,
|
||||
chainId = operation.chain.id,
|
||||
callHash = operation.callHash.value,
|
||||
callInstance = call
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun callDetails(call: GenericCall.Instance): String {
|
||||
return extrinsicGson.toJson(call)
|
||||
}
|
||||
|
||||
override suspend fun callHash(call: GenericCall.Instance, chainId: ChainId): String {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
return call.callHash(runtime).toHexString(withPrefix = true)
|
||||
}
|
||||
|
||||
override suspend fun callDataAsString(call: GenericCall.Instance, chainId: ChainId): String {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
return call.toHex(runtime)
|
||||
}
|
||||
|
||||
override suspend fun isOperationAvailable(operationId: PendingMultisigOperationId): Boolean {
|
||||
val chain = chainRegistry.getChain(operationId.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(operationId.metaId)
|
||||
val callHash = operationId.callHash.intoCallHash()
|
||||
return multisigDetailsRepository.hasMultisigOperation(chain, metaAccount.requireAccountIdKeyIn(chain), callHash)
|
||||
}
|
||||
|
||||
override suspend fun estimateActionFee(operation: PendingMultisigOperation): Fee? {
|
||||
val action = operation.userAction().toInternalAction() ?: return null
|
||||
|
||||
return when (action) {
|
||||
Action.APPROVE -> estimateApproveFee(operation)
|
||||
Action.REJECT -> estimateRejectFee(operation)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performAction(operation: PendingMultisigOperation): Result<ExtrinsicExecutionResult> {
|
||||
val action = operation.userAction().toInternalAction() ?: return Result.failure(IllegalStateException("No action found"))
|
||||
|
||||
return when (action) {
|
||||
Action.APPROVE -> performApprove(operation)
|
||||
Action.REJECT -> performReject(operation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun signatoryFlow(signatoryMetaId: Long): Flow<MetaAccount> {
|
||||
return accountRepository.metaAccountFlow(signatoryMetaId)
|
||||
}
|
||||
|
||||
override suspend fun getSignatoryBalance(signatory: MetaAccount, chain: Chain): Result<ChainAssetBalance> {
|
||||
val asset = chain.utilityAsset
|
||||
val signatoryAccountId = signatory.requireAccountIdIn(chain)
|
||||
return assetSourceRegistry.sourceFor(asset).balance.queryAccountBalanceCatching(chain, asset, signatoryAccountId)
|
||||
}
|
||||
|
||||
override fun isCallValid(operation: PendingMultisigOperation, enteredCall: String): Boolean = runCatching {
|
||||
val operationHash = operation.callHash.value
|
||||
val enteredHash = enteredCall.callHash()
|
||||
|
||||
operationHash.contentEquals(enteredHash)
|
||||
}.getOrDefault(false)
|
||||
|
||||
override fun getSkipRejectConfirmation(): Boolean {
|
||||
return toggleFeatureRepository.get(SKIP_REJECT_CONFIRMATION_KEY, false)
|
||||
}
|
||||
|
||||
override fun setSkipRejectConfirmation(value: Boolean) {
|
||||
toggleFeatureRepository.set(SKIP_REJECT_CONFIRMATION_KEY, value)
|
||||
}
|
||||
|
||||
private suspend fun estimateApproveFee(operation: PendingMultisigOperation): Fee? {
|
||||
if (operation.call == null) return null
|
||||
|
||||
return extrinsicService.estimateFee(
|
||||
chain = operation.chain,
|
||||
origin = TransactionOrigin.WalletWithId(operation.signatoryMetaId)
|
||||
) {
|
||||
// Use zero weight to speed up fee calculation
|
||||
approve(operation, maxWeight = WeightV2.zero())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun estimateRejectFee(operation: PendingMultisigOperation): Fee? {
|
||||
return extrinsicService.estimateFee(
|
||||
chain = operation.chain,
|
||||
origin = TransactionOrigin.WalletWithId(operation.signatoryMetaId)
|
||||
) {
|
||||
// Use zero weight to speed up fee calculation
|
||||
reject(operation)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performApprove(operation: PendingMultisigOperation): Result<ExtrinsicExecutionResult> {
|
||||
val call = operation.call
|
||||
requireNotNull(call) { "Call data not found" }
|
||||
|
||||
return extrinsicService.submitExtrinsicAndAwaitExecution(
|
||||
chain = operation.chain,
|
||||
origin = TransactionOrigin.WalletWithId(operation.signatoryMetaId)
|
||||
) { buildingContext ->
|
||||
val weight = extrinsicSplitter.estimateCallWeight(buildingContext.signer, call, buildingContext.chain)
|
||||
approve(operation, weight)
|
||||
}.requireOk()
|
||||
}
|
||||
|
||||
private suspend fun performReject(operation: PendingMultisigOperation): Result<ExtrinsicExecutionResult> {
|
||||
return extrinsicService.submitExtrinsicAndAwaitExecution(
|
||||
chain = operation.chain,
|
||||
origin = TransactionOrigin.WalletWithId(operation.signatoryMetaId)
|
||||
) {
|
||||
reject(operation)
|
||||
}.requireOk()
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.approve(
|
||||
operation: PendingMultisigOperation,
|
||||
maxWeight: WeightV2
|
||||
) {
|
||||
val selectedAccount = accountRepository.getSelectedMetaAccount() as MultisigMetaAccount
|
||||
|
||||
val approveCall = runtime.composeMultisigAsMulti(
|
||||
multisigMetaAccount = selectedAccount,
|
||||
maybeTimePoint = operation.timePoint,
|
||||
call = operation.call!!,
|
||||
maxWeight = maxWeight
|
||||
)
|
||||
|
||||
call(approveCall)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.reject(operation: PendingMultisigOperation) {
|
||||
val selectedAccount = accountRepository.getSelectedMetaAccount() as MultisigMetaAccount
|
||||
|
||||
val approveCall = runtime.composeMultisigCancelAsMulti(
|
||||
multisigMetaAccount = selectedAccount,
|
||||
maybeTimePoint = operation.timePoint,
|
||||
callHash = operation.callHash
|
||||
)
|
||||
|
||||
call(approveCall)
|
||||
}
|
||||
|
||||
private fun MultisigAction.toInternalAction(): Action? {
|
||||
return when (this) {
|
||||
is MultisigAction.CanApprove -> Action.APPROVE
|
||||
is MultisigAction.CanReject -> Action.REJECT
|
||||
is MultisigAction.Signed -> null
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Action {
|
||||
APPROVE, REJECT
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.domain.details.validations
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class ApproveMultisigOperationValidationFailure {
|
||||
|
||||
class NotEnoughBalanceToPayFees(
|
||||
val signatory: MetaAccount,
|
||||
val chainAsset: Chain.Asset,
|
||||
val minimumNeeded: BigDecimal,
|
||||
val available: BigDecimal
|
||||
) : ApproveMultisigOperationValidationFailure()
|
||||
|
||||
data object TransactionIsNotAvailable : ApproveMultisigOperationValidationFailure()
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.domain.details.validations
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class ApproveMultisigOperationValidationPayload(
|
||||
val fee: Fee,
|
||||
val signatoryBalance: ChainAssetBalance,
|
||||
val signatory: MetaAccount,
|
||||
val operation: PendingMultisigOperation,
|
||||
val multisig: MultisigMetaAccount
|
||||
)
|
||||
|
||||
val ApproveMultisigOperationValidationPayload.chain: Chain
|
||||
get() = operation.chain
|
||||
|
||||
val ApproveMultisigOperationValidationPayload.multisigAccountId: AccountIdKey
|
||||
get() = multisig.requireAccountIdKeyIn(chain)
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.domain.details.validations
|
||||
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.countedTowardsEdAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.transferableAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.validate
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
|
||||
typealias ApproveMultisigOperationValidationSystem = ValidationSystem<ApproveMultisigOperationValidationPayload, ApproveMultisigOperationValidationFailure>
|
||||
typealias ApproveMultisigOperationValidation = Validation<ApproveMultisigOperationValidationPayload, ApproveMultisigOperationValidationFailure>
|
||||
typealias ApproveMultisigOperationValidationSystemBuilder =
|
||||
ValidationSystemBuilder<ApproveMultisigOperationValidationPayload, ApproveMultisigOperationValidationFailure>
|
||||
|
||||
fun ValidationSystem.Companion.approveMultisigOperation(
|
||||
edFactory: EnoughTotalToStayAboveEDValidationFactory,
|
||||
operationStillPendingValidation: OperationIsStillPendingValidation,
|
||||
): ApproveMultisigOperationValidationSystem = ValidationSystem {
|
||||
enoughToPayFeesAndStayAboveEd(edFactory)
|
||||
|
||||
enoughToPayFees()
|
||||
|
||||
validate(operationStillPendingValidation)
|
||||
}
|
||||
|
||||
private fun ApproveMultisigOperationValidationSystemBuilder.enoughToPayFees() {
|
||||
sufficientBalance(
|
||||
fee = { it.fee },
|
||||
available = { it.signatoryBalance.transferableAmount() },
|
||||
error = {
|
||||
ApproveMultisigOperationValidationFailure.NotEnoughBalanceToPayFees(
|
||||
signatory = it.payload.signatory,
|
||||
chainAsset = it.payload.signatoryBalance.chainAsset,
|
||||
minimumNeeded = it.fee,
|
||||
available = it.payload.signatoryBalance.transferableAmount()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ApproveMultisigOperationValidationSystemBuilder.enoughToPayFeesAndStayAboveEd(edFactory: EnoughTotalToStayAboveEDValidationFactory) {
|
||||
edFactory.validate(
|
||||
fee = { it.fee },
|
||||
balance = { it.signatoryBalance.countedTowardsEdAmount() },
|
||||
chainWithAsset = { ChainWithAsset(it.chain, it.signatoryBalance.chainAsset) },
|
||||
error = { payload, errorModel ->
|
||||
ApproveMultisigOperationValidationFailure.NotEnoughBalanceToPayFees(
|
||||
signatory = payload.signatory,
|
||||
chainAsset = payload.signatoryBalance.chainAsset,
|
||||
minimumNeeded = errorModel.minRequiredBalance,
|
||||
available = errorModel.availableBalance
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.domain.details.validations
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.isTrueOrError
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigValidationsRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class OperationIsStillPendingValidation @Inject constructor(
|
||||
private val multisigValidationsRepository: MultisigValidationsRepository
|
||||
) : ApproveMultisigOperationValidation {
|
||||
|
||||
override suspend fun validate(value: ApproveMultisigOperationValidationPayload): ValidationStatus<ApproveMultisigOperationValidationFailure> {
|
||||
val hasPendingCallHash = multisigValidationsRepository.hasPendingCallHash(value.chain.id, value.multisigAccountId, value.operation.callHash)
|
||||
|
||||
return hasPendingCallHash isTrueOrError {
|
||||
ApproveMultisigOperationValidationFailure.TransactionIsNotAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation
|
||||
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
|
||||
interface MultisigOperationsRouter : ReturnableRouter {
|
||||
|
||||
fun openPendingOperations()
|
||||
|
||||
fun openMain()
|
||||
|
||||
fun openMultisigOperationDetails(payload: MultisigOperationDetailsPayload)
|
||||
|
||||
fun openMultisigFullDetails(payload: MultisigOperationPayload)
|
||||
|
||||
fun openEnterCallDetails(payload: MultisigOperationPayload)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting
|
||||
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class MultisigCallDetailsModel(
|
||||
val title: String,
|
||||
val primaryAmount: AmountModel?,
|
||||
val tableEntries: List<TableEntry>,
|
||||
val onBehalfOf: AddressModel?
|
||||
) {
|
||||
|
||||
class TableEntry(
|
||||
val name: String,
|
||||
val value: TableValue
|
||||
)
|
||||
|
||||
sealed class TableValue {
|
||||
|
||||
class Account(val addressModel: AddressModel, val chain: Chain) : TableValue()
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
interface MultisigCallFormatter {
|
||||
|
||||
suspend fun formatPreview(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain,
|
||||
): MultisigCallPreviewModel
|
||||
|
||||
suspend fun formatDetails(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): MultisigCallDetailsModel
|
||||
|
||||
suspend fun formatPushNotificationMessage(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): MultisigCallPushNotificationModel
|
||||
|
||||
suspend fun formatExecutedOperationMessage(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): String
|
||||
|
||||
suspend fun formatRejectedOperationMessage(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
rejectedAccountName: String,
|
||||
chain: Chain
|
||||
): String
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting
|
||||
|
||||
class MultisigCallNotificationModel(val message: String)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting
|
||||
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
|
||||
data class MultisigCallPreviewModel(
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val primaryValue: CharSequence?,
|
||||
val icon: Icon,
|
||||
val onBehalfOf: AddressModel?
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting
|
||||
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
|
||||
class MultisigCallPushNotificationModel(
|
||||
val formattedCall: String,
|
||||
val onBehalfOf: AddressModel?
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetIdWithAmount
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface MultisigActionFormatterDelegate {
|
||||
|
||||
suspend fun formatPreview(visit: CallVisit, chain: Chain): MultisigActionFormatterDelegatePreviewResult?
|
||||
|
||||
suspend fun formatDetails(visit: CallVisit, chain: Chain): MultisigActionFormatterDelegateDetailsResult?
|
||||
|
||||
suspend fun formatMessageCall(visit: CallVisit, chain: Chain): String?
|
||||
}
|
||||
|
||||
class MultisigActionFormatterDelegatePreviewResult(
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val primaryValue: CharSequence?,
|
||||
val icon: Icon,
|
||||
)
|
||||
|
||||
class MultisigActionFormatterDelegateDetailsResult(
|
||||
val title: String,
|
||||
val primaryAmount: ChainAssetIdWithAmount?,
|
||||
val tableEntries: List<TableEntry>,
|
||||
) {
|
||||
|
||||
class TableEntry(
|
||||
val name: String,
|
||||
val value: TableValue
|
||||
)
|
||||
|
||||
sealed class TableValue {
|
||||
|
||||
class Account(val accountId: AccountIdKey, val chain: Chain) : TableValue()
|
||||
}
|
||||
}
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.capitalize
|
||||
import io.novafoundation.nova.common.utils.images.asIcon
|
||||
import io.novafoundation.nova.common.utils.splitAndCapitalizeWords
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallDetailsModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallPreviewModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallPushNotificationModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.AmountConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.TokenConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallTraversal
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.collect
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.extensions.tryFindNonNull
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class RealMultisigCallFormatter @Inject constructor(
|
||||
private val delegates: Set<@JvmSuppressWildcards MultisigActionFormatterDelegate>,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val callTraversal: CallTraversal,
|
||||
@LocalIdentity private val identityProvider: IdentityProvider,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val tokenUseCase: ArbitraryTokenUseCase,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : MultisigCallFormatter {
|
||||
|
||||
override suspend fun formatPreview(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): MultisigCallPreviewModel {
|
||||
return formatCall(
|
||||
call = call,
|
||||
initialOrigin = initialOrigin,
|
||||
chain = chain,
|
||||
formatUnknown = ::formatUnknownPreview,
|
||||
formatDefault = { formatDefaultPreview(it, chain) },
|
||||
formatSpecific = { delegate, callVisit -> delegate.formatPreview(callVisit, chain) },
|
||||
constructFinalResult = { delegateResult, onBehalfOf -> createCallPreview(delegateResult, onBehalfOf) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatDetails(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): MultisigCallDetailsModel {
|
||||
return formatCall(
|
||||
call = call,
|
||||
initialOrigin = initialOrigin,
|
||||
chain = chain,
|
||||
formatUnknown = ::formatUnknownDetails,
|
||||
formatDefault = { formatDetails(it) },
|
||||
formatSpecific = { delegate, callVisit -> delegate.formatDetails(callVisit, chain) },
|
||||
constructFinalResult = { delegateResult, onBehalfOf -> createCallDetails(delegateResult, onBehalfOf) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatPushNotificationMessage(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): MultisigCallPushNotificationModel {
|
||||
return formatCall(
|
||||
call = call,
|
||||
initialOrigin = initialOrigin,
|
||||
chain = chain,
|
||||
formatUnknown = { formatPushNotification(chain, pushUnknownCall()) },
|
||||
formatDefault = { formatPushNotification(chain, it.format()) },
|
||||
formatSpecific = { delegate, callVisit -> delegate.formatMessageCall(callVisit, chain) },
|
||||
constructFinalResult = { delegateResult, onBehalfOf -> formatPushNotification(chain, delegateResult, onBehalfOf) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatExecutedOperationMessage(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): String {
|
||||
return formatCall(
|
||||
call = call,
|
||||
initialOrigin = initialOrigin,
|
||||
chain = chain,
|
||||
formatUnknown = { formatExecutedMessage(chain, dialogUnknownCall()) },
|
||||
formatDefault = { formatExecutedMessage(chain, it.format()) },
|
||||
formatSpecific = { delegate, callVisit -> delegate.formatMessageCall(callVisit, chain) },
|
||||
constructFinalResult = { delegateResult, _ -> formatExecutedMessage(chain, delegateResult) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatRejectedOperationMessage(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
rejectedAccountName: String,
|
||||
chain: Chain
|
||||
): String {
|
||||
return formatCall(
|
||||
call = call,
|
||||
initialOrigin = initialOrigin,
|
||||
chain = chain,
|
||||
formatUnknown = { formatRejectedMessage(chain, dialogUnknownCall(), rejectedAccountName) },
|
||||
formatDefault = { formatRejectedMessage(chain, it.format(), rejectedAccountName) },
|
||||
formatSpecific = { delegate, callVisit -> delegate.formatMessageCall(callVisit, chain) },
|
||||
constructFinalResult = { delegateResult, _ -> formatRejectedMessage(chain, delegateResult, rejectedAccountName) }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <D : Any, R> formatCall(
|
||||
call: GenericCall.Instance?,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain,
|
||||
formatUnknown: () -> R,
|
||||
formatDefault: suspend (GenericCall.Instance) -> R,
|
||||
formatSpecific: suspend (MultisigActionFormatterDelegate, CallVisit) -> D?,
|
||||
constructFinalResult: suspend (D, onBehalfOf: AddressModel?) -> R
|
||||
): R {
|
||||
if (call == null) return formatUnknown()
|
||||
|
||||
val firstFormattedCall = callTraversal.collect(call, initialOrigin)
|
||||
.map { formatCallVisit(it) { delegate -> formatSpecific(delegate, it) } }
|
||||
.getFirstFormated()
|
||||
|
||||
return if (firstFormattedCall != null) {
|
||||
val (singleMatch, singleMatchVisit) = firstFormattedCall
|
||||
|
||||
val onBehalfOf = createOnBehalfOf(singleMatchVisit, initialOrigin, chain)
|
||||
return constructFinalResult(singleMatch!!, onBehalfOf)
|
||||
} else {
|
||||
formatDefault(call)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DelegateResultWithVisit<*>.isFormatted() = first != null
|
||||
|
||||
private fun <T> List<DelegateResultWithVisit<T>>.getFirstFormated(): DelegateResultWithVisit<T>? {
|
||||
return firstOrNull { it.isFormatted() }
|
||||
}
|
||||
|
||||
private fun createCallPreview(
|
||||
delegateResult: MultisigActionFormatterDelegatePreviewResult,
|
||||
onBehalfOf: AddressModel?
|
||||
): MultisigCallPreviewModel {
|
||||
return with(delegateResult) { MultisigCallPreviewModel(title, subtitle, primaryValue, icon, onBehalfOf) }
|
||||
}
|
||||
|
||||
private suspend fun createCallDetails(
|
||||
delegateResult: MultisigActionFormatterDelegateDetailsResult,
|
||||
onBehalfOf: AddressModel?
|
||||
): MultisigCallDetailsModel {
|
||||
return MultisigCallDetailsModel(
|
||||
title = delegateResult.title,
|
||||
primaryAmount = delegateResult.primaryAmount?.let {
|
||||
val token = tokenUseCase.getToken(it.chainAssetId)
|
||||
amountFormatter.formatAmountToAmountModel(
|
||||
it.amount,
|
||||
token,
|
||||
config = AmountConfig(
|
||||
tokenConfig = TokenConfig(tokenAmountSign = AmountSign.NEGATIVE)
|
||||
)
|
||||
)
|
||||
},
|
||||
tableEntries = delegateResult.tableEntries.map { it.toUi() },
|
||||
onBehalfOf = onBehalfOf
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MultisigActionFormatterDelegateDetailsResult.TableEntry.toUi(): MultisigCallDetailsModel.TableEntry {
|
||||
return MultisigCallDetailsModel.TableEntry(
|
||||
name = name,
|
||||
value = value.toUi()
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MultisigActionFormatterDelegateDetailsResult.TableValue.toUi(): MultisigCallDetailsModel.TableValue {
|
||||
return when (this) {
|
||||
is MultisigActionFormatterDelegateDetailsResult.TableValue.Account -> {
|
||||
val addressModel = addressIconGenerator.createAccountAddressModel(
|
||||
chain = chain,
|
||||
accountId = accountId.value,
|
||||
name = identityProvider.identityFor(accountId.value, chain.id)?.name
|
||||
)
|
||||
|
||||
MultisigCallDetailsModel.TableValue.Account(addressModel, chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createOnBehalfOf(
|
||||
callVisit: CallVisit,
|
||||
initialOrigin: AccountIdKey,
|
||||
chain: Chain
|
||||
): AddressModel? {
|
||||
if (callVisit.callOrigin == initialOrigin) return null
|
||||
|
||||
val onBehalfOf = callVisit.callOrigin.value
|
||||
|
||||
return addressIconGenerator.createAccountAddressModel(
|
||||
chain = chain,
|
||||
accountId = onBehalfOf,
|
||||
name = identityProvider.identityFor(onBehalfOf, chain.id)?.name
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <D : Any> formatCallVisit(
|
||||
callVisit: CallVisit,
|
||||
format: suspend (MultisigActionFormatterDelegate) -> D?
|
||||
): DelegateResultWithVisit<D> {
|
||||
val result = delegates.tryFindNonNull { format(it) }
|
||||
return result to callVisit
|
||||
}
|
||||
|
||||
private fun formatDefaultPreview(call: GenericCall.Instance, chain: Chain): MultisigCallPreviewModel {
|
||||
return MultisigCallPreviewModel(
|
||||
title = call.function.name.splitAndCapitalizeWords(),
|
||||
subtitle = call.module.name.splitAndCapitalizeWords(),
|
||||
primaryValue = null,
|
||||
icon = assetIconProvider.multisigFormatAssetIcon(chain),
|
||||
onBehalfOf = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatUnknownPreview(): MultisigCallPreviewModel {
|
||||
return MultisigCallPreviewModel(
|
||||
title = resourceManager.getString(R.string.multisig_operations_unknown_calldata),
|
||||
subtitle = null,
|
||||
primaryValue = null,
|
||||
icon = R.drawable.ic_unknown_operation.asIcon(),
|
||||
onBehalfOf = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatDetails(call: GenericCall.Instance): MultisigCallDetailsModel {
|
||||
return MultisigCallDetailsModel(
|
||||
title = call.function.name.splitAndCapitalizeWords(),
|
||||
primaryAmount = null,
|
||||
tableEntries = emptyList(),
|
||||
onBehalfOf = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatUnknownDetails(): MultisigCallDetailsModel {
|
||||
return MultisigCallDetailsModel(
|
||||
title = resourceManager.getString(R.string.multisig_operations_unknown_calldata),
|
||||
primaryAmount = null,
|
||||
tableEntries = emptyList(),
|
||||
onBehalfOf = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatPushNotification(chain: Chain, formattedCall: String, onBehalfOf: AddressModel? = null): MultisigCallPushNotificationModel {
|
||||
return MultisigCallPushNotificationModel(
|
||||
resourceManager.getString(
|
||||
R.string.multisig_notification_init_transaction_message,
|
||||
formattedCall,
|
||||
chain.name
|
||||
),
|
||||
onBehalfOf = onBehalfOf
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatExecutedMessage(chain: Chain, formattedCall: String): String {
|
||||
return resourceManager.getString(
|
||||
R.string.multisig_transaction_executed_dialog_message,
|
||||
formattedCall,
|
||||
chain.name
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatRejectedMessage(chain: Chain, formattedCall: String, rejectedAccountName: String): String {
|
||||
return resourceManager.getString(
|
||||
R.string.multisig_transaction_rejected_dialog_message,
|
||||
formattedCall,
|
||||
chain.name,
|
||||
rejectedAccountName
|
||||
)
|
||||
}
|
||||
|
||||
private fun pushUnknownCall() = resourceManager.getString(R.string.multisig_operations_unknown_calldata)
|
||||
|
||||
private fun dialogUnknownCall() = resourceManager.getString(R.string.multisig_transaction_dialog_message_unknown_call)
|
||||
|
||||
private fun GenericCall.Instance.format(): String {
|
||||
return resourceManager.getString(R.string.multisig_operation_default_call_format, module.name.capitalize(), function.name.capitalize())
|
||||
}
|
||||
}
|
||||
|
||||
private typealias DelegateResultWithVisit<T> = Pair<T?, CallVisit>
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.images.asIcon
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.getNameOrAddress
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters.MultisigActionFormatterDelegateDetailsResult.TableEntry
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters.MultisigActionFormatterDelegateDetailsResult.TableValue
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.tryParseTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.toIdWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatToken
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.TokenConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.extensions.tryFindNonNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class TransferMultisigActionFormatter @Inject constructor(
|
||||
@LocalIdentity private val identityProvider: IdentityProvider,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val tokenFormatter: TokenFormatter
|
||||
) : MultisigActionFormatterDelegate {
|
||||
|
||||
override suspend fun formatPreview(
|
||||
visit: CallVisit,
|
||||
chain: Chain
|
||||
): MultisigActionFormatterDelegatePreviewResult? {
|
||||
val parsedTransfer = tryParseTransfer(visit, chain) ?: return null
|
||||
|
||||
val destAddress = chain.addressOf(parsedTransfer.destination)
|
||||
|
||||
return MultisigActionFormatterDelegatePreviewResult(
|
||||
title = resourceManager.getString(R.string.transfer_title),
|
||||
subtitle = resourceManager.getString(R.string.transfer_history_send_to, destAddress),
|
||||
primaryValue = parsedTransfer.amount.formatAmount(),
|
||||
icon = R.drawable.ic_arrow_up.asIcon()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatDetails(visit: CallVisit, chain: Chain): MultisigActionFormatterDelegateDetailsResult? {
|
||||
val parsedTransfer = tryParseTransfer(visit, chain) ?: return null
|
||||
|
||||
return MultisigActionFormatterDelegateDetailsResult(
|
||||
title = resourceManager.getString(R.string.transfer_title),
|
||||
primaryAmount = parsedTransfer.amount.toIdWithAmount(),
|
||||
tableEntries = listOf(
|
||||
TableEntry(
|
||||
name = resourceManager.getString(R.string.wallet_recipient),
|
||||
value = TableValue.Account(parsedTransfer.destination, chain)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatMessageCall(visit: CallVisit, chain: Chain): String? {
|
||||
val parsedTransfer = tryParseTransfer(visit, chain) ?: return null
|
||||
|
||||
val accountName = identityProvider.getNameOrAddress(parsedTransfer.destination, chain)
|
||||
val formattedAmount = parsedTransfer.amount.formatAmount(withSign = false)
|
||||
|
||||
return resourceManager.getString(
|
||||
R.string.multisig_transaction_message_transfer,
|
||||
formattedAmount,
|
||||
accountName
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun tryParseTransfer(
|
||||
visit: CallVisit,
|
||||
chain: Chain
|
||||
): TransferParsedFromCall? {
|
||||
return assetSourceRegistry.allSources().tryFindNonNull {
|
||||
it.transfers.tryParseTransfer(visit.call, chain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ChainAssetWithAmount.formatAmount(withSign: Boolean = true): CharSequence {
|
||||
return if (withSign) {
|
||||
tokenFormatter.formatToken(amount, chainAsset, config = TokenConfig(tokenAmountSign = AmountSign.NEGATIVE))
|
||||
} else {
|
||||
tokenFormatter.formatToken(amount, chainAsset)
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.capitalize
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class UtilityBatchesActionFormatter @Inject constructor(
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val resourceManager: ResourceManager,
|
||||
) : MultisigActionFormatterDelegate {
|
||||
|
||||
override suspend fun formatPreview(
|
||||
visit: CallVisit,
|
||||
chain: Chain
|
||||
): MultisigActionFormatterDelegatePreviewResult? {
|
||||
val batchCallFormat = visit.formatCall() ?: return null
|
||||
|
||||
return MultisigActionFormatterDelegatePreviewResult(
|
||||
title = batchCallFormat,
|
||||
subtitle = visit.call.module.name.capitalize(),
|
||||
primaryValue = null,
|
||||
icon = assetIconProvider.multisigFormatAssetIcon(chain)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatDetails(visit: CallVisit, chain: Chain): MultisigActionFormatterDelegateDetailsResult? {
|
||||
val batchCallFormat = visit.formatCall() ?: return null
|
||||
|
||||
return MultisigActionFormatterDelegateDetailsResult(
|
||||
title = batchCallFormat,
|
||||
primaryAmount = null,
|
||||
tableEntries = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun formatMessageCall(visit: CallVisit, chain: Chain): String? {
|
||||
val batchCallFormat = visit.formatCall() ?: return null
|
||||
|
||||
return resourceManager.getString(
|
||||
R.string.multisig_operation_default_call_format,
|
||||
visit.call.module.name.capitalize(),
|
||||
batchCallFormat
|
||||
)
|
||||
}
|
||||
|
||||
private fun CallVisit.formatCall(): String? {
|
||||
if (call.module.name != Modules.UTILITY) return null
|
||||
|
||||
return when (call.function.name) {
|
||||
"batch" -> resourceManager.getString(R.string.multisig_operation_utility_batch_title)
|
||||
"batch_all" -> resourceManager.getString(R.string.multisig_operation_utility_batch_all_title)
|
||||
"force_batch" -> resourceManager.getString(R.string.multisig_operation_utility_force_batch_title)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.formatters
|
||||
|
||||
import io.novafoundation.nova.common.data.model.AssetIconMode
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
fun AssetIconProvider.multisigFormatAssetIcon(chain: Chain) = getAssetIconOrFallback(chain.utilityAsset, AssetIconMode.WHITE)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.common
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class MultisigOperationPayload(
|
||||
val chainId: String,
|
||||
val metaId: Long,
|
||||
val callHash: String
|
||||
) : Parcelable {
|
||||
companion object;
|
||||
}
|
||||
|
||||
fun MultisigOperationPayload.Companion.fromOperationId(operationId: PendingMultisigOperationId): MultisigOperationPayload {
|
||||
return MultisigOperationPayload(
|
||||
chainId = operationId.chainId,
|
||||
metaId = operationId.metaId,
|
||||
callHash = operationId.callHash
|
||||
)
|
||||
}
|
||||
|
||||
fun MultisigOperationPayload.toOperationId(): PendingMultisigOperationId {
|
||||
return PendingMultisigOperationId(
|
||||
chainId = chainId,
|
||||
metaId = metaId,
|
||||
callHash = callHash
|
||||
)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.created
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.fragment.ActionBottomSheetDialogFragment
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureComponent
|
||||
|
||||
class MultisigCreatedBottomSheet : ActionBottomSheetDialogFragment<MultisigCreatedViewModel>() {
|
||||
|
||||
companion object : PayloadCreator<MultisigCreatedPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<MultisigOperationsFeatureComponent>(requireContext(), MultisigOperationsFeatureApi::class.java)
|
||||
.multisigCreated()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: MultisigCreatedViewModel) {}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.created
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class MultisigCreatedPayload(
|
||||
val walletWasSwitched: Boolean
|
||||
) : Parcelable
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.created
|
||||
|
||||
import android.view.Gravity
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.highlightedText
|
||||
import io.novafoundation.nova.common.view.AlertModel
|
||||
import io.novafoundation.nova.common.view.AlertView
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetPayload
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ButtonPreferences
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.fragment.ActionBottomSheetViewModel
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.primary
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.secondary
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
|
||||
class MultisigCreatedViewModel(
|
||||
private val resourceManager: ResourceManager,
|
||||
private val router: MultisigOperationsRouter,
|
||||
private val payload: MultisigCreatedPayload
|
||||
) : ActionBottomSheetViewModel() {
|
||||
|
||||
override fun getPayload(): ActionBottomSheetPayload {
|
||||
return ActionBottomSheetPayload(
|
||||
imageRes = R.drawable.ic_multisig,
|
||||
title = resourceManager.getString(R.string.multisig_transaction_created_title),
|
||||
subtitle = getSubtitle(),
|
||||
actionButtonPreferences = ButtonPreferences.primary(primaryButtonText()),
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(secondaryButtonText()),
|
||||
alertModel = getAlertModel(),
|
||||
checkBoxPreferences = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAlertModel(): AlertModel? {
|
||||
return if (payload.walletWasSwitched) {
|
||||
AlertModel(
|
||||
style = AlertView.Style.fromPreset(AlertView.StylePreset.INFO, iconGravity = Gravity.CENTER),
|
||||
message = resourceManager.getString(R.string.alert_nova_has_selected_ms_wallet)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionClicked() {
|
||||
router.openPendingOperations()
|
||||
}
|
||||
|
||||
private fun getSubtitle() = resourceManager.highlightedText(
|
||||
mainRes = R.string.multisig_transaction_created_subtitle,
|
||||
R.string.multisig_transaction_created_subtitle_highlight
|
||||
)
|
||||
|
||||
private fun primaryButtonText() = resourceManager.getString(R.string.multisig_transaction_created_view_details)
|
||||
private fun secondaryButtonText() = resourceManager.getString(R.string.common_close)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.created.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedBottomSheet
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
MultisigCreatedModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface MultisigCreatedComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: MultisigCreatedPayload
|
||||
): MultisigCreatedComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: MultisigCreatedBottomSheet)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.created.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class MultisigCreatedModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(MultisigCreatedViewModel::class)
|
||||
fun provideViewModel(
|
||||
resourceManager: ResourceManager,
|
||||
router: MultisigOperationsRouter,
|
||||
payload: MultisigCreatedPayload
|
||||
): ViewModel {
|
||||
return MultisigCreatedViewModel(resourceManager, router, payload)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): MultisigCreatedViewModel {
|
||||
return ViewModelProvider(
|
||||
fragment,
|
||||
viewModelFactory
|
||||
).get(MultisigCreatedViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink
|
||||
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.ACTION
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.CALL_HASH_PARAM
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.CHAIN_ID_PARAM
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.MULTISIG_ADDRESS_PARAM
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.SCREEN
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.utils.doIf
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.DeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilder
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.addParamIfNotNull
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.ACTOR_IDENTITY_PARAM
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.CALL_DATA_PARAM
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.OPERATION_STATE_PARAM
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator.Companion.SIGNATORY_ADDRESS_PARAM
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
|
||||
class MultisigOperationDeepLinkData(
|
||||
val chainId: String,
|
||||
val multisigAddress: String,
|
||||
val signatoryAddress: String,
|
||||
val callHash: String,
|
||||
val callData: String?,
|
||||
val operationState: State?,
|
||||
) {
|
||||
sealed interface State {
|
||||
companion object;
|
||||
|
||||
data object Active : State
|
||||
class Rejected(val actorIdentity: String) : State
|
||||
class Executed(val actorIdentity: String) : State
|
||||
}
|
||||
}
|
||||
|
||||
interface MultisigOperationDeepLinkConfigurator : DeepLinkConfigurator<MultisigOperationDeepLinkData> {
|
||||
|
||||
companion object {
|
||||
const val ACTION = "open"
|
||||
const val SCREEN = "multisigOperation"
|
||||
const val PREFIX = "/$ACTION/$SCREEN"
|
||||
const val CHAIN_ID_PARAM = "chainId"
|
||||
const val MULTISIG_ADDRESS_PARAM = "multisigAddress"
|
||||
const val SIGNATORY_ADDRESS_PARAM = "signatoryAddress"
|
||||
const val ACTOR_IDENTITY_PARAM = "actorIdentity"
|
||||
const val CALL_HASH_PARAM = "callHash"
|
||||
const val CALL_DATA_PARAM = "callData"
|
||||
const val OPERATION_STATE_PARAM = "operationState"
|
||||
}
|
||||
}
|
||||
|
||||
class RealMultisigOperationDeepLinkConfigurator(
|
||||
private val linkBuilderFactory: LinkBuilderFactory
|
||||
) : MultisigOperationDeepLinkConfigurator {
|
||||
|
||||
override fun configure(payload: MultisigOperationDeepLinkData, type: DeepLinkConfigurator.Type): Uri {
|
||||
// We not add Polkadot chain id to simplify deep link
|
||||
val appendChainIdParam = payload.chainId != ChainGeneses.POLKADOT
|
||||
|
||||
return linkBuilderFactory.newLink(type)
|
||||
.setAction(ACTION)
|
||||
.setScreen(SCREEN)
|
||||
.doIf(appendChainIdParam) { addParam(CHAIN_ID_PARAM, payload.chainId) }
|
||||
.addState(payload.operationState)
|
||||
.addParam(MULTISIG_ADDRESS_PARAM, payload.multisigAddress)
|
||||
.addParam(SIGNATORY_ADDRESS_PARAM, payload.signatoryAddress)
|
||||
.addParam(CALL_HASH_PARAM, payload.callHash)
|
||||
.addParamIfNotNull(CALL_DATA_PARAM, payload.callData)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.getOperationState(): MultisigOperationDeepLinkData.State? {
|
||||
return MultisigOperationDeepLinkData.State.fromString(getQueryParameter(OPERATION_STATE_PARAM), this)
|
||||
}
|
||||
|
||||
private fun LinkBuilder.addState(state: MultisigOperationDeepLinkData.State?): LinkBuilder {
|
||||
return addParamIfNotNull(OPERATION_STATE_PARAM, state?.mapToString())
|
||||
.addParamIfNotNull(ACTOR_IDENTITY_PARAM, state?.actorIdentityOrNull())
|
||||
}
|
||||
|
||||
private fun MultisigOperationDeepLinkData.State.mapToString() = when (this) {
|
||||
MultisigOperationDeepLinkData.State.Active -> "active"
|
||||
is MultisigOperationDeepLinkData.State.Executed -> "executed"
|
||||
is MultisigOperationDeepLinkData.State.Rejected -> "rejected"
|
||||
}
|
||||
|
||||
private fun MultisigOperationDeepLinkData.State.Companion.fromString(value: String?, uri: Uri) = when (value) {
|
||||
"active" -> MultisigOperationDeepLinkData.State.Active
|
||||
"executed" -> MultisigOperationDeepLinkData.State.Executed(uri.getQueryParameter(ACTOR_IDENTITY_PARAM)!!)
|
||||
"rejected" -> MultisigOperationDeepLinkData.State.Rejected(uri.getQueryParameter(ACTOR_IDENTITY_PARAM)!!)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun MultisigOperationDeepLinkData.State.actorIdentityOrNull() = when (this) {
|
||||
MultisigOperationDeepLinkData.State.Active -> null
|
||||
is MultisigOperationDeepLinkData.State.Executed -> actorIdentity
|
||||
is MultisigOperationDeepLinkData.State.Rejected -> actorIdentity
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.utils.DialogMessageManager
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.create
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.accountIdKeyIn
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.fromOperationId
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.ext.toAccountIdKey
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
class MultisigOperationDetailsDeepLinkHandler(
|
||||
private val router: MultisigOperationsRouter,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val automaticInteractionGate: AutomaticInteractionGate,
|
||||
private val dialogMessageManager: DialogMessageManager,
|
||||
private val multisigCallFormatter: MultisigCallFormatter,
|
||||
) : DeepLinkHandler {
|
||||
|
||||
override val callbackFlow: Flow<CallbackEvent> = emptyFlow()
|
||||
|
||||
override suspend fun matches(data: Uri): Boolean {
|
||||
val path = data.path ?: return false
|
||||
|
||||
return path.startsWith(MultisigOperationDeepLinkConfigurator.PREFIX)
|
||||
}
|
||||
|
||||
override suspend fun handleDeepLink(data: Uri): Result<Unit> = runCatching {
|
||||
automaticInteractionGate.awaitInteractionAllowed()
|
||||
|
||||
val chainId = data.getChainId()
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
|
||||
val multisigAccount = data.getMultisigAddress()?.toAccountIdKey(chain) ?: error("Multisig address not found")
|
||||
val signatoryAccount = data.getSignatoryAddress()?.toAccountIdKey(chain) ?: error("Signatory address not found")
|
||||
val callHash = data.getCallHash() ?: error("Call hash not found")
|
||||
val callData = data.getCallData(chainId)
|
||||
val operationState = data.getOperationState()
|
||||
|
||||
val multisigMetaAccount = accountRepository.getActiveMetaAccounts()
|
||||
.filterIsInstance<MultisigMetaAccount>()
|
||||
.firstOrNull { it.accountIdKeyIn(chain) == multisigAccount && it.signatoryAccountId == signatoryAccount }
|
||||
?: error("Multisig account not found")
|
||||
|
||||
accountRepository.selectMetaAccount(multisigMetaAccount.id)
|
||||
|
||||
when (operationState) {
|
||||
null,
|
||||
MultisigOperationDeepLinkData.State.Active -> {
|
||||
val operationIdentifier = PendingMultisigOperationId.create(multisigMetaAccount, chain, callHash.removeHexPrefix())
|
||||
val operationPayload = MultisigOperationPayload.fromOperationId(operationIdentifier)
|
||||
router.openMultisigOperationDetails(
|
||||
MultisigOperationDetailsPayload(
|
||||
operationPayload,
|
||||
navigationButtonMode = MultisigOperationDetailsPayload.NavigationButtonMode.CLOSE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is MultisigOperationDeepLinkData.State.Executed -> showDialog(
|
||||
R.string.multisig_transaction_executed_dialog_title,
|
||||
multisigCallFormatter.formatExecutedOperationMessage(callData, signatoryAccount, chain)
|
||||
)
|
||||
|
||||
is MultisigOperationDeepLinkData.State.Rejected -> showDialog(
|
||||
R.string.multisig_transaction_rejected_dialog_title,
|
||||
multisigCallFormatter.formatRejectedOperationMessage(callData, signatoryAccount, operationState.actorIdentity, chain)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog(titleRes: Int, messageText: String) {
|
||||
dialogMessageManager.showDialog {
|
||||
setTitle(titleRes)
|
||||
setMessage(messageText)
|
||||
setPositiveButton(R.string.common_got_it, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Uri.getChainId(): String {
|
||||
return getQueryParameter(MultisigOperationDeepLinkConfigurator.CHAIN_ID_PARAM) ?: ChainGeneses.POLKADOT
|
||||
}
|
||||
|
||||
private fun Uri.getMultisigAddress(): String? {
|
||||
return getQueryParameter(MultisigOperationDeepLinkConfigurator.MULTISIG_ADDRESS_PARAM)
|
||||
}
|
||||
|
||||
private fun Uri.getSignatoryAddress(): String? {
|
||||
return getQueryParameter(MultisigOperationDeepLinkConfigurator.SIGNATORY_ADDRESS_PARAM)
|
||||
}
|
||||
|
||||
private fun Uri.getCallHash(): String? {
|
||||
return getQueryParameter(MultisigOperationDeepLinkConfigurator.CALL_HASH_PARAM)
|
||||
}
|
||||
|
||||
private suspend fun Uri.getCallData(chainId: String): GenericCall.Instance? {
|
||||
val callDataString = getQueryParameter(MultisigOperationDeepLinkConfigurator.CALL_DATA_PARAM) ?: return null
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
return GenericCall.fromHex(runtime, callDataString)
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.full
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.copy.setupCopyText
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.view.bottomSheet.description.observeDescription
|
||||
import io.novafoundation.nova.common.view.showValueOrHide
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.view.showAccountWithLoading
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.FragmentMultisigOperationFullDetailsBinding
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.showAmount
|
||||
|
||||
class MultisigOperationFullDetailsFragment : BaseFragment<MultisigOperationFullDetailsViewModel, FragmentMultisigOperationFullDetailsBinding>() {
|
||||
|
||||
companion object : PayloadCreator<MultisigOperationPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun createBinding() = FragmentMultisigOperationFullDetailsBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.multisigPendingOperationFullDetailsToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
binder.multisigPendingOperationDetailsDepositor.setOnClickListener { viewModel.onDepositorClicked() }
|
||||
binder.multisigPendingOperationDetailsDeposit.setOnClickListener { viewModel.depositClicked() }
|
||||
binder.multisigPendingOperationDetailsCallHash.setOnClickListener { viewModel.callHashClicked() }
|
||||
binder.multisigPendingOperationDetailsCallData.setOnClickListener { viewModel.callDataClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<MultisigOperationsFeatureComponent>(
|
||||
requireContext(),
|
||||
MultisigOperationsFeatureApi::class.java
|
||||
)
|
||||
.multisigOperationFullDetails()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: MultisigOperationFullDetailsViewModel) {
|
||||
observeDescription(viewModel)
|
||||
setupExternalActions(viewModel)
|
||||
setupCopyText(viewModel)
|
||||
|
||||
viewModel.depositorAccountModel.observe { binder.multisigPendingOperationDetailsDepositor.showAccountWithLoading(it) }
|
||||
viewModel.depositAmount.observe { binder.multisigPendingOperationDetailsDeposit.showAmount(it) }
|
||||
viewModel.ellipsizedCallHash.observe { binder.multisigPendingOperationDetailsCallHash.showValueOrHide(it, null) }
|
||||
viewModel.ellipsizedCallData.observe { binder.multisigPendingOperationDetailsCallData.showValueOrHide(it, null) }
|
||||
viewModel.formattedCall.observe { binder.multisigPendingOperationDetailsCall.text = it }
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.full
|
||||
|
||||
import io.novafoundation.nova.common.address.toHexWithPrefix
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.domain.onLoaded
|
||||
import io.novafoundation.nova.common.mixin.copy.CopyTextLauncher
|
||||
import io.novafoundation.nova.common.mixin.copy.showCopyCallHash
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.ellipsizeMiddle
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.utils.withSafeLoading
|
||||
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.MultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.toOperationId
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val CALL_HASH_SHOWN_SYMBOLS = 9
|
||||
|
||||
class MultisigOperationFullDetailsViewModel(
|
||||
private val router: MultisigOperationsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val interactor: MultisigOperationDetailsInteractor,
|
||||
private val multisigOperationsService: MultisigPendingOperationsService,
|
||||
private val externalActions: ExternalActions.Presentation,
|
||||
private val payload: MultisigOperationPayload,
|
||||
private val accountUIUseCase: AccountUIUseCase,
|
||||
private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher,
|
||||
private val copyTextLauncher: CopyTextLauncher.Presentation,
|
||||
private val arbitraryTokenUseCase: ArbitraryTokenUseCase,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : BaseViewModel(),
|
||||
ExternalActions by externalActions,
|
||||
CopyTextLauncher by copyTextLauncher,
|
||||
DescriptionBottomSheetLauncher by descriptionBottomSheetLauncher {
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
private val operationFlow = multisigOperationsService.pendingOperationFlow(payload.toOperationId())
|
||||
.filterNotNull()
|
||||
.shareInBackground()
|
||||
|
||||
private val tokenFlow = operationFlow.map {
|
||||
arbitraryTokenUseCase.getToken(it.chain.utilityAsset.fullId)
|
||||
}.shareInBackground()
|
||||
|
||||
val depositorAccountModel = operationFlow.map {
|
||||
accountUIUseCase.getAccountModel(it.depositor, it.chain)
|
||||
}.withSafeLoading()
|
||||
.shareInBackground()
|
||||
|
||||
val depositAmount = combine(operationFlow, tokenFlow) { operation, token ->
|
||||
amountFormatter.formatAmountToAmountModel(operation.deposit, token)
|
||||
}.shareInBackground()
|
||||
|
||||
private val callDataFlow = operationFlow.map { operation ->
|
||||
operation.call?.let { interactor.callDataAsString(it, operation.chain.id) }
|
||||
}.shareInBackground()
|
||||
|
||||
val ellipsizedCallData = callDataFlow.map { it?.ellipsizeMiddle(CALL_HASH_SHOWN_SYMBOLS) }
|
||||
|
||||
val formattedCall = operationFlow.map { operation ->
|
||||
operation.call?.let { interactor.callDetails(it) }
|
||||
}.shareInBackground()
|
||||
|
||||
private val callHash = operationFlow.map { operation ->
|
||||
operation.callHash.toHexWithPrefix()
|
||||
}.shareInBackground()
|
||||
|
||||
val ellipsizedCallHash = callHash.map {
|
||||
it.ellipsizeMiddle(CALL_HASH_SHOWN_SYMBOLS)
|
||||
}.shareInBackground()
|
||||
|
||||
fun onDepositorClicked() = launchUnit {
|
||||
val chain = operationFlow.first().chain
|
||||
depositorAccountModel.first().onLoaded {
|
||||
externalActions.showAddressActions(it.address(), chain)
|
||||
}
|
||||
}
|
||||
|
||||
fun callDataClicked() = launchUnit {
|
||||
val callDataEllipsized = ellipsizedCallData.first() ?: return@launchUnit
|
||||
val callData = callDataFlow.first() ?: return@launchUnit
|
||||
copyTextLauncher.showCopyTextDialog(
|
||||
CopyTextLauncher.Payload(
|
||||
title = callDataEllipsized.toString(),
|
||||
textToCopy = callData,
|
||||
resourceManager.getString(R.string.common_copy_call_data),
|
||||
resourceManager.getString(R.string.common_share_call_data)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun depositClicked() {
|
||||
launchDescriptionBottomSheet(
|
||||
titleRes = R.string.multisig_deposit,
|
||||
descriptionRes = R.string.multisig_deposit_description
|
||||
)
|
||||
}
|
||||
|
||||
fun callHashClicked() = launchUnit {
|
||||
copyTextLauncher.showCopyCallHash(resourceManager, callHash.first())
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.full.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.full.MultisigOperationFullDetailsFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
MultisigOperationFullDetailsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface MultisigOperationFullDetailsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: MultisigOperationPayload,
|
||||
): MultisigOperationFullDetailsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: MultisigOperationFullDetailsFragment)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.full.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.copy.CopyTextLauncher
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.MultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.full.MultisigOperationFullDetailsViewModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.di.MultisigOperationDetailsModule.BindsModule
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
|
||||
@Module(includes = [ViewModelModule::class, BindsModule::class])
|
||||
class MultisigOperationFullDetailsModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(MultisigOperationFullDetailsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: MultisigOperationsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
interactor: MultisigOperationDetailsInteractor,
|
||||
multisigOperationsService: MultisigPendingOperationsService,
|
||||
externalActions: ExternalActions.Presentation,
|
||||
payload: MultisigOperationPayload,
|
||||
descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher,
|
||||
copyTextLauncher: CopyTextLauncher.Presentation,
|
||||
accountUIUseCase: AccountUIUseCase,
|
||||
arbitraryTokenUseCase: ArbitraryTokenUseCase,
|
||||
amountFormatter: AmountFormatter
|
||||
): ViewModel {
|
||||
return MultisigOperationFullDetailsViewModel(
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
interactor = interactor,
|
||||
multisigOperationsService = multisigOperationsService,
|
||||
externalActions = externalActions,
|
||||
payload = payload,
|
||||
descriptionBottomSheetLauncher = descriptionBottomSheetLauncher,
|
||||
copyTextLauncher = copyTextLauncher,
|
||||
accountUIUseCase = accountUIUseCase,
|
||||
arbitraryTokenUseCase = arbitraryTokenUseCase,
|
||||
amountFormatter = amountFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): MultisigOperationFullDetailsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(MultisigOperationFullDetailsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.domain.isLoaded
|
||||
import io.novafoundation.nova.common.domain.isLoading
|
||||
import io.novafoundation.nova.common.domain.onLoaded
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.setupConfirmationDialog
|
||||
import io.novafoundation.nova.common.mixin.impl.observeValidations
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.common.view.TableCellView
|
||||
import io.novafoundation.nova.common.view.bindWithHideShowButton
|
||||
import io.novafoundation.nova.common.view.setExtraInfoAvailable
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.observeActionBottomSheet
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.common.view.shape.addRipple
|
||||
import io.novafoundation.nova.common.view.shape.getBlockDrawable
|
||||
import io.novafoundation.nova.feature_account_api.view.showWallet
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.view.showAddress
|
||||
import io.novafoundation.nova.feature_account_api.view.showAddressOrHide
|
||||
import io.novafoundation.nova.feature_account_api.view.showChain
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.FragmentMultisigOperationDetailsBinding
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallDetailsModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.adapter.SignatoriesAdapter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.amount.setAmountOrHide
|
||||
|
||||
class MultisigOperationDetailsFragment : BaseFragment<MultisigOperationDetailsViewModel, FragmentMultisigOperationDetailsBinding>() {
|
||||
|
||||
companion object : PayloadCreator<MultisigOperationDetailsPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun createBinding() = FragmentMultisigOperationDetailsBinding.inflate(layoutInflater)
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) { SignatoriesAdapter(viewModel::onSignatoryClicked) }
|
||||
|
||||
override fun initViews() {
|
||||
binder.multisigPendingOperationDetailsToolbar.setHomeButtonIcon(viewModel.getNavigationIconRes())
|
||||
binder.multisigPendingOperationDetailsToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
binder.multisigOperationSignatories.adapter = adapter
|
||||
|
||||
binder.multisigPendingOperationDetailsEnterCallData.setOnClickListener { viewModel.enterCallDataClicked() }
|
||||
binder.multisigPendingOperationDetailsAction.prepareForProgress(viewLifecycleOwner)
|
||||
binder.multisigPendingOperationDetailsAction.setOnClickListener { viewModel.actionClicked() }
|
||||
|
||||
binder.multisigPendingOperationCallDetails.setOnClickListener { viewModel.callDetailsClicked() }
|
||||
binder.multisigPendingOperationCallDetails.background = with(requireContext()) {
|
||||
addRipple(getBlockDrawable())
|
||||
}
|
||||
|
||||
binder.multisigPendingOperationDetailsWallet.setOnClickListener { viewModel.walletDetailsClicked() }
|
||||
binder.multisigPendingOperationDetailsBehalfOf.setOnClickListener { viewModel.behalfOfClicked() }
|
||||
binder.multisigPendingOperationDetailsSignatory.setOnClickListener { viewModel.signatoryDetailsClicked() }
|
||||
|
||||
binder.multisigOperationSignatoriesContainer.bindWithHideShowButton(binder.multisigOperationShowHideButton)
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<MultisigOperationsFeatureComponent>(
|
||||
requireContext(),
|
||||
MultisigOperationsFeatureApi::class.java
|
||||
)
|
||||
.multisigOperationDetails()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: MultisigOperationDetailsViewModel) {
|
||||
observeValidations(viewModel)
|
||||
setupExternalActions(viewModel)
|
||||
setupFeeLoading(viewModel.feeLoaderMixin, binder.multisigPendingOperationDetailsFee)
|
||||
observeActionBottomSheet(viewModel.actionBottomSheetLauncher)
|
||||
setupConfirmationDialog(R.style.AccentAlertDialogTheme, viewModel.operationNotFoundAwaitableAction)
|
||||
|
||||
viewModel.isOperationLoadingFlow.observe {
|
||||
binder.multisigPendingOperationProgress.isVisible = it
|
||||
binder.multisigPendingOperationDetailsContainer.isGone = it
|
||||
}
|
||||
|
||||
viewModel.showCallButtonState.observe(binder.multisigPendingOperationDetailsEnterCallData::isVisible::set)
|
||||
viewModel.actionButtonState.observe(binder.multisigPendingOperationDetailsAction::setState)
|
||||
viewModel.buttonAppearance.observe(binder.multisigPendingOperationDetailsAction::setAppearance)
|
||||
|
||||
viewModel.chainUiFlow.observe(binder.multisigPendingOperationDetailsNetwork::showChain)
|
||||
viewModel.walletFlow.observe(binder.multisigPendingOperationDetailsWallet::showWallet)
|
||||
|
||||
viewModel.formattedCall.observe {
|
||||
binder.multisigPendingOperationDetailsBehalfOf.showAddressOrHide(it.onBehalfOf)
|
||||
binder.multisigPendingOperationDetailsToolbar.setTitle(it.title)
|
||||
binder.multisigPendingOperationPrimaryAmount.setAmountOrHide(it.primaryAmount)
|
||||
|
||||
showFormattedCallTable(it.tableEntries)
|
||||
}
|
||||
|
||||
viewModel.signatoryAccount.observe(binder.multisigPendingOperationDetailsSignatory::showWallet)
|
||||
|
||||
viewModel.signatoriesTitle.observe(binder.multisigOperationSignatoriesTitle::setText)
|
||||
viewModel.formattedSignatories.observe { signatoriesLoadingState ->
|
||||
binder.multisigOperationSignatoriesShimmering.isVisible = signatoriesLoadingState.isLoading
|
||||
binder.multisigOperationSignatories.isVisible = signatoriesLoadingState.isLoaded()
|
||||
signatoriesLoadingState.onLoaded { adapter.submitList(it) }
|
||||
}
|
||||
|
||||
viewModel.callDetailsVisible.observe(binder.multisigPendingOperationCallDetails::setVisible)
|
||||
}
|
||||
|
||||
private fun showFormattedCallTable(tableEntries: List<MultisigCallDetailsModel.TableEntry>) {
|
||||
binder.multisigPendingOperationDetailsCallTable.removeAllViews()
|
||||
|
||||
tableEntries.forEach {
|
||||
val entryView = createFormattedCallEntryView(it)
|
||||
binder.multisigPendingOperationDetailsCallTable.addView(entryView)
|
||||
}
|
||||
|
||||
binder.multisigPendingOperationDetailsCallTable.invalidateChildrenVisibility()
|
||||
}
|
||||
|
||||
private fun createFormattedCallEntryView(entry: MultisigCallDetailsModel.TableEntry): TableCellView {
|
||||
return TableCellView(requireContext()).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
|
||||
setTitle(entry.name)
|
||||
|
||||
when (val value = entry.value) {
|
||||
is MultisigCallDetailsModel.TableValue.Account -> {
|
||||
setExtraInfoAvailable(true)
|
||||
showAddress(value.addressModel)
|
||||
setOnClickListener { viewModel.onTableAccountClicked(value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class MultisigOperationDetailsPayload(
|
||||
val operation: MultisigOperationPayload,
|
||||
val navigationButtonMode: NavigationButtonMode = NavigationButtonMode.BACK
|
||||
) : Parcelable {
|
||||
enum class NavigationButtonMode {
|
||||
BACK, CLOSE
|
||||
}
|
||||
}
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.base.TitleAndMessage
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.common.domain.dataOrNull
|
||||
import io.novafoundation.nova.common.domain.isLoading
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationDialogInfo
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.fromRes
|
||||
import io.novafoundation.nova.common.mixin.api.Validatable
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.bold
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.SpannableFormatter
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.format
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.highlightedText
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.utils.withLoadingShared
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.progressConsumer
|
||||
import io.novafoundation.nova.common.view.PrimaryButton
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ButtonPreferences
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.CheckBoxPreferences
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.allSignatories
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.MultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.validations.ApproveMultisigOperationValidationFailure
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.validations.ApproveMultisigOperationValidationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.validations.ApproveMultisigOperationValidationSystem
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallDetailsModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.toOperationId
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.adapter.SignatoryRvItem
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.connectWith
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.createDefault
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class MultisigOperationDetailsViewModel(
|
||||
private val router: MultisigOperationsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val interactor: MultisigOperationDetailsInteractor,
|
||||
private val multisigOperationsService: MultisigPendingOperationsService,
|
||||
private val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory,
|
||||
private val externalActions: ExternalActions.Presentation,
|
||||
private val validationExecutor: ValidationExecutor,
|
||||
private val payload: MultisigOperationDetailsPayload,
|
||||
private val validationSystem: ApproveMultisigOperationValidationSystem,
|
||||
private val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper,
|
||||
private val signatoryListFormatter: SignatoryListFormatter,
|
||||
private val multisigCallFormatter: MultisigCallFormatter,
|
||||
private val actionBottomSheetLauncherFactory: ActionBottomSheetLauncherFactory,
|
||||
private val accountInteractor: AccountInteractor,
|
||||
private val accountUIUseCase: AccountUIUseCase,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val amountFormatter: AmountFormatter,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
) : BaseViewModel(),
|
||||
Validatable by validationExecutor,
|
||||
ExternalActions by externalActions {
|
||||
|
||||
val operationNotFoundAwaitableAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
|
||||
|
||||
private val operationFlow = multisigOperationsService.pendingOperationFlow(payload.operation.toOperationId())
|
||||
.filterNotNull()
|
||||
.shareInBackground()
|
||||
|
||||
private val isLastOperationFlow = flowOf {
|
||||
val operationsCount = multisigOperationsService.getPendingOperationsCount()
|
||||
operationsCount == 1
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
private val chainFlow = operationFlow.map { it.chain }
|
||||
.shareInBackground()
|
||||
|
||||
private val chainAssetFlow = chainFlow.map { it.utilityAsset }
|
||||
.shareInBackground()
|
||||
|
||||
val chainUiFlow = chainFlow.map { mapChainToUi(it) }
|
||||
.shareInBackground()
|
||||
|
||||
private val selectedAccountFlow = selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
.filterIsInstance<MultisigMetaAccount>()
|
||||
.shareInBackground()
|
||||
|
||||
val walletFlow = walletUiUseCase.selectedWalletUiFlow(showAddressIcon = true)
|
||||
.shareInBackground()
|
||||
|
||||
val formattedCall = combine(
|
||||
selectedAccountFlow,
|
||||
operationFlow
|
||||
) { metaAccount, operation ->
|
||||
val initialOrigin = metaAccount.requireAccountIdKeyIn(operation.chain)
|
||||
multisigCallFormatter.formatDetails(operation.call, initialOrigin, operation.chain)
|
||||
}.shareInBackground()
|
||||
|
||||
private val signatory = operationFlow
|
||||
.map { it.signatoryMetaId }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest(interactor::signatoryFlow)
|
||||
.shareInBackground()
|
||||
|
||||
val signatoryAccount = signatory.map { walletUiUseCase.walletUiFor(it) }
|
||||
.shareInBackground()
|
||||
|
||||
val signatoriesTitle = combine(
|
||||
selectedAccountFlow,
|
||||
operationFlow
|
||||
) { metaAccount, operation ->
|
||||
resourceManager.getString(R.string.multisig_operation_details_signatories, operation.approvals.size, metaAccount.threshold)
|
||||
}.shareInBackground()
|
||||
|
||||
private val signatoryAccounts = selectedAccountFlow.map { it.allSignatories() }
|
||||
.distinctUntilChanged()
|
||||
.map { accountUIUseCase.getAccountModels(it, chainFlow.first()) }
|
||||
.shareInBackground()
|
||||
|
||||
val formattedSignatories = combine(
|
||||
signatory,
|
||||
signatoryAccounts,
|
||||
operationFlow
|
||||
) { currentSignatory, allSignatories, operation ->
|
||||
signatoryListFormatter.formatSignatories(
|
||||
chain = chainFlow.first(),
|
||||
currentSignatory = currentSignatory,
|
||||
signatories = allSignatories,
|
||||
approvals = operation.approvals.toSet()
|
||||
)
|
||||
}.withLoadingShared()
|
||||
.shareInBackground()
|
||||
|
||||
private val showNextProgress = MutableStateFlow(false)
|
||||
|
||||
val feeLoaderMixin = feeLoaderMixinV2Factory.createDefault(viewModelScope, chainAssetFlow)
|
||||
|
||||
val showCallButtonState = operationFlow.map { it.call == null }
|
||||
.shareInBackground()
|
||||
|
||||
val actionButtonState = combine(showNextProgress, operationFlow) { submissionInProgress, operation ->
|
||||
val action = operation.userAction()
|
||||
|
||||
when {
|
||||
submissionInProgress -> DescriptiveButtonState.Loading
|
||||
|
||||
action is MultisigAction.CanApprove -> when {
|
||||
|
||||
operation.call == null -> DescriptiveButtonState.Gone
|
||||
|
||||
action.isFinalApproval -> DescriptiveButtonState.Enabled(
|
||||
action = resourceManager.getString(R.string.multisig_operation_details_approve_and_execute)
|
||||
)
|
||||
|
||||
else -> DescriptiveButtonState.Enabled(
|
||||
action = resourceManager.getString(R.string.multisig_operation_details_approve)
|
||||
)
|
||||
}
|
||||
|
||||
action is MultisigAction.CanReject -> DescriptiveButtonState.Enabled(
|
||||
action = resourceManager.getString(R.string.multisig_operation_details_reject)
|
||||
)
|
||||
|
||||
else -> DescriptiveButtonState.Gone
|
||||
}
|
||||
}.shareInBackground()
|
||||
|
||||
val buttonAppearance = operationFlow.map { operation ->
|
||||
when {
|
||||
operation.userAction() is MultisigAction.CanReject -> PrimaryButton.Appearance.PRIMARY_NEGATIVE
|
||||
else -> PrimaryButton.Appearance.PRIMARY
|
||||
}
|
||||
}.shareInBackground()
|
||||
|
||||
val callDetailsVisible = operationFlow
|
||||
.map { operation -> operation.call != null }
|
||||
.shareInBackground()
|
||||
|
||||
val actionBottomSheetLauncher = actionBottomSheetLauncherFactory.create()
|
||||
|
||||
val isOperationLoadingFlow = operationFlow.withLoadingShared()
|
||||
.map { it.isLoading }
|
||||
.shareInBackground()
|
||||
|
||||
init {
|
||||
checkOperationAvailability()
|
||||
|
||||
loadFee()
|
||||
}
|
||||
|
||||
private fun checkOperationAvailability() = launchUnit {
|
||||
val isOperationAvailable = interactor.isOperationAvailable(payload.operation.toOperationId())
|
||||
|
||||
if (!isOperationAvailable) {
|
||||
showErrorAndCloseScreen()
|
||||
}
|
||||
}
|
||||
|
||||
fun enterCallDataClicked() {
|
||||
router.openEnterCallDetails(payload.operation)
|
||||
}
|
||||
|
||||
fun actionClicked() {
|
||||
launch {
|
||||
val operation = operationFlow.first()
|
||||
|
||||
val isReject = operation.userAction() == MultisigAction.CanReject
|
||||
if (isReject) {
|
||||
confirmReject(operation.getDepositorName())
|
||||
}
|
||||
|
||||
sendTransactionIfValid()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PendingMultisigOperation.getDepositorName(): String {
|
||||
val depositorAccount = withContext(Dispatchers.Default) { accountInteractor.findMetaAccount(chain, depositor.value) }
|
||||
return depositorAccount?.name ?: chain.addressOf(depositor)
|
||||
}
|
||||
|
||||
private suspend fun confirmReject(depositorName: String) = suspendCancellableCoroutine<Unit> {
|
||||
if (interactor.getSkipRejectConfirmation()) {
|
||||
it.resume(Unit)
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
var isAutoContinueChecked = false
|
||||
|
||||
actionBottomSheetLauncher.launchBottomSheet(
|
||||
imageRes = R.drawable.ic_multisig,
|
||||
title = resourceManager.getString(R.string.multisig_signing_warning_title),
|
||||
subtitle = resourceManager.highlightedText(R.string.multisig_signing_reject_confirmation_subtitle, depositorName),
|
||||
actionButtonPreferences = ButtonPreferences(
|
||||
text = resourceManager.getString(R.string.common_confirm),
|
||||
style = PrimaryButton.Appearance.PRIMARY,
|
||||
onClick = {
|
||||
interactor.setSkipRejectConfirmation(isAutoContinueChecked)
|
||||
it.resume(Unit)
|
||||
}
|
||||
),
|
||||
neutralButtonPreferences = ButtonPreferences(
|
||||
text = resourceManager.getString(R.string.common_cancel),
|
||||
style = PrimaryButton.Appearance.SECONDARY,
|
||||
onClick = { it.cancel() }
|
||||
),
|
||||
checkBoxPreferences = CheckBoxPreferences(
|
||||
text = resourceManager.getString(R.string.common_check_box_auto_continue),
|
||||
onCheckChanged = { isAutoContinueChecked = it }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun callDetailsClicked() = launch {
|
||||
router.openMultisigFullDetails(payload.operation)
|
||||
}
|
||||
|
||||
private fun loadFee() {
|
||||
feeLoaderMixin.connectWith(operationFlow) { _, operation ->
|
||||
interactor.estimateActionFee(operation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTransactionIfValid() = launchUnit {
|
||||
showNextProgress.value = true
|
||||
|
||||
val signatory = signatory.first()
|
||||
val operation = operationFlow.first()
|
||||
|
||||
val signatoryBalance = interactor.getSignatoryBalance(signatory, operation.chain)
|
||||
.onFailure {
|
||||
showError(it)
|
||||
showNextProgress.value = false
|
||||
}
|
||||
.getOrNull() ?: return@launchUnit
|
||||
|
||||
val payload = ApproveMultisigOperationValidationPayload(
|
||||
fee = feeLoaderMixin.awaitFee(),
|
||||
signatoryBalance = signatoryBalance,
|
||||
signatory = signatory,
|
||||
operation = operation,
|
||||
multisig = selectedAccountFlow.first()
|
||||
)
|
||||
|
||||
validationExecutor.requireValid(
|
||||
validationSystem = validationSystem,
|
||||
payload = payload,
|
||||
validationFailureTransformer = ::formatValidationFailure,
|
||||
progressConsumer = showNextProgress.progressConsumer()
|
||||
) {
|
||||
sendTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatValidationFailure(failure: ApproveMultisigOperationValidationFailure): TitleAndMessage {
|
||||
return when (failure) {
|
||||
is ApproveMultisigOperationValidationFailure.NotEnoughBalanceToPayFees -> {
|
||||
val title = resourceManager.getString(R.string.common_error_not_enough_tokens)
|
||||
val message = SpannableFormatter.format(
|
||||
resourceManager,
|
||||
R.string.multisig_signatory_validation_ed,
|
||||
failure.signatory.name.bold(),
|
||||
failure.minimumNeeded.formatTokenAmount(failure.chainAsset),
|
||||
failure.available.formatTokenAmount(failure.chainAsset),
|
||||
)
|
||||
|
||||
title to message
|
||||
}
|
||||
|
||||
ApproveMultisigOperationValidationFailure.TransactionIsNotAvailable -> {
|
||||
resourceManager.getString(R.string.multisig_approve_transaction_unavailable_title) to
|
||||
resourceManager.getString(R.string.multisig_approve_transaction_unavailable_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTransaction() = launch {
|
||||
interactor.performAction(operationFlow.first())
|
||||
.onFailure(::showError)
|
||||
.onSuccess {
|
||||
showToast(resourceManager.getString(R.string.common_transaction_submitted))
|
||||
|
||||
extrinsicNavigationWrapper.startNavigation(it.submissionHierarchy) {
|
||||
val isLeastOperation = isLastOperationFlow.first()
|
||||
|
||||
if (isLeastOperation) {
|
||||
router.openMain()
|
||||
} else {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showNextProgress.value = false
|
||||
}
|
||||
|
||||
fun onSignatoryClicked(signatoryRvItem: SignatoryRvItem) = launchUnit {
|
||||
showAddressActionForOriginChain(signatoryRvItem.accountModel.address())
|
||||
}
|
||||
|
||||
fun walletDetailsClicked() = launchUnit {
|
||||
val metaAccount = selectedAccountFlow.first()
|
||||
val chain = chainFlow.first()
|
||||
externalActions.showAddressActions(metaAccount, chain)
|
||||
}
|
||||
|
||||
fun onTableAccountClicked(tableAccount: MultisigCallDetailsModel.TableValue.Account) = launchUnit {
|
||||
externalActions.showAddressActions(tableAccount.addressModel.address, tableAccount.chain)
|
||||
}
|
||||
|
||||
fun behalfOfClicked() = launchUnit {
|
||||
val behalfOf = formattedCall.first().onBehalfOf ?: return@launchUnit
|
||||
showAddressActionForOriginChain(behalfOf.address)
|
||||
}
|
||||
|
||||
fun signatoryDetailsClicked() = launchUnit {
|
||||
val metaAccount = signatory.first()
|
||||
val chain = chainFlow.first()
|
||||
externalActions.showAddressActions(metaAccount, chain)
|
||||
}
|
||||
|
||||
fun getNavigationIconRes() = when (payload.navigationButtonMode) {
|
||||
MultisigOperationDetailsPayload.NavigationButtonMode.BACK -> R.drawable.ic_arrow_back
|
||||
MultisigOperationDetailsPayload.NavigationButtonMode.CLOSE -> R.drawable.ic_close
|
||||
}
|
||||
|
||||
private suspend fun showAddressActionForOriginChain(address: String) {
|
||||
externalActions.showAddressActions(address, chainFlow.first())
|
||||
}
|
||||
|
||||
private suspend fun showErrorAndCloseScreen() {
|
||||
val confirmationInfo = ConfirmationDialogInfo.fromRes(
|
||||
resourceManager,
|
||||
title = R.string.multisig_operation_details_not_found_title,
|
||||
message = R.string.multisig_operation_details_not_found_message,
|
||||
positiveButton = R.string.common_got_it,
|
||||
negativeButton = null
|
||||
)
|
||||
operationNotFoundAwaitableAction.awaitAction(confirmationInfo)
|
||||
router.back()
|
||||
}
|
||||
|
||||
private fun isOperationWasExecuted(
|
||||
old: ExtendedLoadingState<Boolean>,
|
||||
new: ExtendedLoadingState<Boolean>
|
||||
) = old.dataOrNull == true && new.dataOrNull == false
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.list.GroupedList
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountModel
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.ProxiedMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.delegeted.MultisigFormatter
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.delegeted.ProxyFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.adapter.SignatoryRvItem
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class SignatoryListFormatter(
|
||||
private val accountInteractor: AccountInteractor,
|
||||
private val proxyFormatter: ProxyFormatter,
|
||||
private val multisigFormatter: MultisigFormatter
|
||||
) {
|
||||
|
||||
private class FormattingContext(
|
||||
val currentSignatoryAccountId: AccountIdKey,
|
||||
val currentSignatory: MetaAccount,
|
||||
val metaAccountByAccountIds: GroupedList<AccountIdKey, MetaAccount>,
|
||||
val metaAccountsByMetaId: Map<Long, MetaAccount>,
|
||||
val approvals: Set<AccountIdKey>
|
||||
) {
|
||||
|
||||
/**
|
||||
* We try to show the most relevant account to user.
|
||||
* For signatory account that associated with current multisig account we like to show the same account
|
||||
*/
|
||||
fun account(accountId: AccountIdKey): MetaAccount? {
|
||||
if (currentSignatoryAccountId == accountId) return currentSignatory
|
||||
|
||||
return metaAccountByAccountIds[accountId]
|
||||
?.findRelevantAccountToShow()
|
||||
}
|
||||
|
||||
fun account(metaId: Long) = metaAccountsByMetaId[metaId]
|
||||
|
||||
fun isApprovedBy(accountId: AccountIdKey): Boolean = accountId in approvals
|
||||
}
|
||||
|
||||
suspend fun formatSignatories(
|
||||
chain: Chain,
|
||||
currentSignatory: MetaAccount,
|
||||
signatories: Map<AccountIdKey, AccountModel>,
|
||||
approvals: Set<AccountIdKey>
|
||||
): List<SignatoryRvItem> {
|
||||
val metaAccounts = accountInteractor.getActiveMetaAccounts()
|
||||
val formattingContext = formattingContext(chain, currentSignatory, metaAccounts, approvals)
|
||||
|
||||
return signatories.map { (signatoryAccountId, signatoryAccountModel) ->
|
||||
val maybeMetaAccount = formattingContext.account(signatoryAccountId)
|
||||
SignatoryRvItem(
|
||||
accountModel = signatoryAccountModel,
|
||||
subtitle = maybeMetaAccount?.formatSubtitle(formattingContext),
|
||||
isApproved = formattingContext.isApprovedBy(signatoryAccountId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formattingContext(
|
||||
chain: Chain,
|
||||
currentSignatory: MetaAccount,
|
||||
metaAccounts: List<MetaAccount>,
|
||||
approvals: Set<AccountIdKey>
|
||||
): FormattingContext {
|
||||
val metaAccountsByAccountIds = metaAccounts
|
||||
.filter { it.hasAccountIn(chain) }
|
||||
.groupBy { it.requireAccountIdKeyIn(chain) }
|
||||
|
||||
val metaAccountsByMetaIds = metaAccounts.associateBy { it.id }
|
||||
|
||||
return FormattingContext(
|
||||
currentSignatory.requireAccountIdKeyIn(chain),
|
||||
currentSignatory,
|
||||
metaAccountsByAccountIds,
|
||||
metaAccountsByMetaIds,
|
||||
approvals
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MetaAccount.formatSubtitle(context: FormattingContext): CharSequence? = when (this) {
|
||||
is ProxiedMetaAccount -> formatSubtitle(context)
|
||||
is MultisigMetaAccount -> formatSubtitle(context)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private suspend fun ProxiedMetaAccount.formatSubtitle(context: FormattingContext): CharSequence? {
|
||||
val proxyMetaAccount = context.account(this.proxy.proxyMetaId) ?: return null
|
||||
|
||||
return proxyFormatter.mapProxyMetaAccountSubtitle(
|
||||
proxyMetaAccount.name,
|
||||
proxyFormatter.makeAccountDrawable(proxyMetaAccount),
|
||||
proxy
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MultisigMetaAccount.formatSubtitle(context: FormattingContext): CharSequence? {
|
||||
val signatory = context.account(this.signatoryMetaId) ?: return null
|
||||
|
||||
return multisigFormatter.formatSignatorySubtitle(signatory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we may have multiple accounts by one account id it would be better to show the most relevant to user.
|
||||
* For example we show secrets-account instead of the proxied-account
|
||||
*/
|
||||
private fun Collection<MetaAccount>.findRelevantAccountToShow(): MetaAccount? {
|
||||
return minByOrNull { it.priorityToShowAsSignatory() }
|
||||
}
|
||||
|
||||
private fun MetaAccount.priorityToShowAsSignatory() = when (type) {
|
||||
LightMetaAccount.Type.SECRETS -> 0
|
||||
|
||||
LightMetaAccount.Type.PARITY_SIGNER,
|
||||
LightMetaAccount.Type.LEDGER_LEGACY,
|
||||
LightMetaAccount.Type.LEDGER,
|
||||
LightMetaAccount.Type.POLKADOT_VAULT,
|
||||
LightMetaAccount.Type.WATCH_ONLY -> 1
|
||||
|
||||
LightMetaAccount.Type.PROXIED,
|
||||
LightMetaAccount.Type.MULTISIG -> 2
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import coil.clear
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.relevantEllipsizeMode
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.ItemMultisigSignatoryAccountBinding
|
||||
|
||||
class SignatoriesAdapter(
|
||||
private val handler: Handler
|
||||
) : ListAdapter<SignatoryRvItem, SignatoryViewHolder>(SignatoriesDiffCallback()) {
|
||||
|
||||
fun interface Handler {
|
||||
fun onSignatoryClicked(signatory: SignatoryRvItem)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SignatoryViewHolder {
|
||||
return SignatoryViewHolder(
|
||||
ItemMultisigSignatoryAccountBinding.inflate(parent.inflater(), parent, false),
|
||||
handler
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SignatoryViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
class SignatoryViewHolder(
|
||||
private val binder: ItemMultisigSignatoryAccountBinding,
|
||||
private val itemHandler: SignatoriesAdapter.Handler,
|
||||
) : BaseViewHolder(binder.root) {
|
||||
|
||||
fun bind(item: SignatoryRvItem) = with(binder) {
|
||||
root.setOnClickListener { itemHandler.onSignatoryClicked(item) }
|
||||
itemSignatoryAccountIcon.setImageDrawable(item.accountModel.drawable())
|
||||
itemSignatoryAccountSelected.setVisible(item.isApproved, falseState = View.INVISIBLE)
|
||||
itemSignatoryAccountTitle.text = item.accountModel.nameOrAddress()
|
||||
itemSignatoryAccountTitle.ellipsize = item.accountModel.relevantEllipsizeMode()
|
||||
itemSignatoryAccountSubtitle.setTextOrHide(item.subtitle)
|
||||
}
|
||||
|
||||
override fun unbind() {
|
||||
with(binder) {
|
||||
itemSignatoryAccountIcon.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SignatoriesDiffCallback : DiffUtil.ItemCallback<SignatoryRvItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: SignatoryRvItem, newItem: SignatoryRvItem): Boolean {
|
||||
return oldItem.accountModel == newItem.accountModel
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SignatoryRvItem, newItem: SignatoryRvItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general.adapter
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountModel
|
||||
|
||||
data class SignatoryRvItem(
|
||||
val accountModel: AccountModel,
|
||||
val subtitle: CharSequence?,
|
||||
val isApproved: Boolean,
|
||||
)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsFragment
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
MultisigOperationDetailsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface MultisigOperationDetailsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: MultisigOperationDetailsPayload,
|
||||
): MultisigOperationDetailsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: MultisigOperationDetailsFragment)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.details.general.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.MultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.RealMultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.validations.ApproveMultisigOperationValidationSystem
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.validations.OperationIsStillPendingValidation
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.validations.approveMultisigOperation
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsViewModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.SignatoryListFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.di.MultisigOperationDetailsModule.BindsModule
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
|
||||
@Module(includes = [ViewModelModule::class, BindsModule::class])
|
||||
class MultisigOperationDetailsModule {
|
||||
|
||||
@Module
|
||||
interface BindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindInteractor(real: RealMultisigOperationDetailsInteractor): MultisigOperationDetailsInteractor
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideValidationSystem(
|
||||
edValidationFactory: EnoughTotalToStayAboveEDValidationFactory,
|
||||
operationIsStillPendingValidation: OperationIsStillPendingValidation
|
||||
): ApproveMultisigOperationValidationSystem {
|
||||
return ValidationSystem.approveMultisigOperation(edValidationFactory, operationIsStillPendingValidation)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(MultisigOperationDetailsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: MultisigOperationsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
interactor: MultisigOperationDetailsInteractor,
|
||||
multisigOperationsService: MultisigPendingOperationsService,
|
||||
feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory,
|
||||
externalActions: ExternalActions.Presentation,
|
||||
validationExecutor: ValidationExecutor,
|
||||
payload: MultisigOperationDetailsPayload,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
validationSystem: ApproveMultisigOperationValidationSystem,
|
||||
extrinsicNavigationWrapper: ExtrinsicNavigationWrapper,
|
||||
signatoryListFormatter: SignatoryListFormatter,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
multisigCallFormatter: MultisigCallFormatter,
|
||||
actionBottomSheetLauncherFactory: ActionBottomSheetLauncherFactory,
|
||||
accountInteractor: AccountInteractor,
|
||||
accountUIUseCase: AccountUIUseCase,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
amountFormatter: AmountFormatter,
|
||||
): ViewModel {
|
||||
return MultisigOperationDetailsViewModel(
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
interactor = interactor,
|
||||
multisigOperationsService = multisigOperationsService,
|
||||
feeLoaderMixinV2Factory = feeLoaderMixinV2Factory,
|
||||
externalActions = externalActions,
|
||||
validationExecutor = validationExecutor,
|
||||
payload = payload,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
validationSystem = validationSystem,
|
||||
extrinsicNavigationWrapper = extrinsicNavigationWrapper,
|
||||
signatoryListFormatter = signatoryListFormatter,
|
||||
multisigCallFormatter = multisigCallFormatter,
|
||||
actionBottomSheetLauncherFactory = actionBottomSheetLauncherFactory,
|
||||
accountInteractor = accountInteractor,
|
||||
accountUIUseCase = accountUIUseCase,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixinFactory,
|
||||
amountFormatter = amountFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): MultisigOperationDetailsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(MultisigOperationDetailsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.enterCall
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.insets.applySystemBarInsets
|
||||
import io.novafoundation.nova.common.utils.bindTo
|
||||
import io.novafoundation.nova.common.utils.insets.ImeInsetsState
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.FragmentMultisigOperationEnterCallBinding
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
|
||||
class MultisigOperationEnterCallFragment : BaseFragment<MultisigOperationEnterCallViewModel, FragmentMultisigOperationEnterCallBinding>() {
|
||||
|
||||
companion object : PayloadCreator<MultisigOperationPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun createBinding() = FragmentMultisigOperationEnterCallBinding.inflate(layoutInflater)
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<MultisigOperationsFeatureComponent>(
|
||||
requireContext(),
|
||||
MultisigOperationsFeatureApi::class.java
|
||||
)
|
||||
.multisigOperationEnterCall()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.root.applySystemBarInsets(imeInsets = ImeInsetsState.ENABLE_IF_SUPPORTED)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.multisigOperationEnterCallToolbar.setHomeButtonListener { viewModel.back() }
|
||||
|
||||
binder.multisigOperationEnterCallAction.setOnClickListener { viewModel.approve() }
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: MultisigOperationEnterCallViewModel) {
|
||||
binder.multisigOperationEnterCallInput.content.bindTo(viewModel.enteredCall, viewLifecycleOwner.lifecycleScope)
|
||||
|
||||
viewModel.buttonState.observe {
|
||||
binder.multisigOperationEnterCallAction.setState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.enterCall
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.callHashString
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.MultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.toOperationId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class MultisigOperationEnterCallViewModel(
|
||||
private val router: MultisigOperationsRouter,
|
||||
private val interactor: MultisigOperationDetailsInteractor,
|
||||
private val multisigOperationsService: MultisigPendingOperationsService,
|
||||
private val payload: MultisigOperationPayload,
|
||||
private val resourceManager: ResourceManager
|
||||
) : BaseViewModel() {
|
||||
|
||||
val enteredCall = MutableStateFlow("")
|
||||
|
||||
val buttonState = enteredCall.map {
|
||||
when {
|
||||
it.isBlank() -> DescriptiveButtonState.Disabled(reason = resourceManager.getString(R.string.enter_call_data_title))
|
||||
else -> DescriptiveButtonState.Enabled(action = resourceManager.getString(R.string.common_save))
|
||||
}
|
||||
}
|
||||
|
||||
fun back() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun approve() = launchUnit {
|
||||
val operation = multisigOperationsService.pendingOperation(payload.toOperationId()) ?: return@launchUnit
|
||||
if (interactor.isCallValid(operation, enteredCall.value)) {
|
||||
interactor.setCall(operation, enteredCall.value)
|
||||
router.back()
|
||||
} else {
|
||||
onCallInvalid(enteredCall.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCallInvalid(enteredCall: String) = try {
|
||||
val callHash = enteredCall.callHashString()
|
||||
showError(
|
||||
resourceManager.getString(R.string.invalid_call_data_title),
|
||||
resourceManager.getString(R.string.invalid_call_data_message, callHash)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showError(
|
||||
resourceManager.getString(R.string.invalid_call_data_title),
|
||||
resourceManager.getString(R.string.invalid_call_data_format_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.enterCall.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.enterCall.MultisigOperationEnterCallFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
MultisigOperationEnterCallModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface MultisigOperationEnterCallComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: MultisigOperationPayload,
|
||||
): MultisigOperationEnterCallComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: MultisigOperationEnterCallFragment)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.enterCall.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.MultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.domain.details.RealMultisigOperationDetailsInteractor
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.di.MultisigOperationDetailsModule.BindsModule
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.enterCall.MultisigOperationEnterCallViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class, BindsModule::class])
|
||||
class MultisigOperationEnterCallModule {
|
||||
|
||||
@Module
|
||||
interface BindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindInteractor(real: RealMultisigOperationDetailsInteractor): MultisigOperationDetailsInteractor
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(MultisigOperationEnterCallViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: MultisigOperationsRouter,
|
||||
interactor: MultisigOperationDetailsInteractor,
|
||||
multisigOperationsService: MultisigPendingOperationsService,
|
||||
payload: MultisigOperationPayload,
|
||||
resourceManager: ResourceManager
|
||||
): ViewModel {
|
||||
return MultisigOperationEnterCallViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
multisigOperationsService = multisigOperationsService,
|
||||
payload = payload,
|
||||
resourceManager = resourceManager,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): MultisigOperationEnterCallViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(MultisigOperationEnterCallViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.BaseGroupedDiffCallback
|
||||
import io.novafoundation.nova.common.list.GroupedListAdapter
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.presentation.setColoredText
|
||||
import io.novafoundation.nova.common.utils.formatting.formatDaysSinceEpoch
|
||||
import io.novafoundation.nova.common.utils.images.setIcon
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.letOrHide
|
||||
import io.novafoundation.nova.common.utils.setDrawableEnd
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.view.shape.addRipple
|
||||
import io.novafoundation.nova.common.view.shape.getBottomRoundedCornerDrawable
|
||||
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.ItemMultisigPendingOperationBinding
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.ItemMultisigPendingOperationHeaderBinding
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.model.PendingMultisigOperationHeaderModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.model.PendingMultisigOperationModel
|
||||
|
||||
class MultisigPendingOperationsAdapter(
|
||||
private val handler: ItemHandler,
|
||||
private val imageLoader: ImageLoader,
|
||||
) : GroupedListAdapter<PendingMultisigOperationHeaderModel, PendingMultisigOperationModel>(PendingMultisigOperationDiffCallback()) {
|
||||
|
||||
interface ItemHandler {
|
||||
|
||||
fun itemClicked(model: PendingMultisigOperationModel)
|
||||
}
|
||||
|
||||
override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return MultisigPendingOperationHeaderHolder(
|
||||
viewBinding = ItemMultisigPendingOperationHeaderBinding.inflate(parent.inflater(), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return MultisigPendingOperationHolder(
|
||||
viewBinding = ItemMultisigPendingOperationBinding.inflate(parent.inflater(), parent, false),
|
||||
itemHandler = handler,
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
}
|
||||
|
||||
override fun bindChild(holder: GroupedListHolder, child: PendingMultisigOperationModel) {
|
||||
(holder as MultisigPendingOperationHolder).bind(child)
|
||||
}
|
||||
|
||||
override fun bindGroup(holder: GroupedListHolder, group: PendingMultisigOperationHeaderModel) {
|
||||
(holder as MultisigPendingOperationHeaderHolder).bind(group)
|
||||
}
|
||||
}
|
||||
|
||||
class MultisigPendingOperationHeaderHolder(
|
||||
private val viewBinding: ItemMultisigPendingOperationHeaderBinding,
|
||||
) : GroupedListHolder(viewBinding.root) {
|
||||
|
||||
fun bind(model: PendingMultisigOperationHeaderModel) {
|
||||
viewBinding.itemMultisigPendingOperationHeader.text = model.daysSinceEpoch.formatDaysSinceEpoch(viewBinding.root.context)
|
||||
}
|
||||
}
|
||||
|
||||
class MultisigPendingOperationHolder(
|
||||
private val viewBinding: ItemMultisigPendingOperationBinding,
|
||||
private val itemHandler: MultisigPendingOperationsAdapter.ItemHandler,
|
||||
private val imageLoader: ImageLoader,
|
||||
) : GroupedListHolder(viewBinding.root) {
|
||||
|
||||
init {
|
||||
with(viewBinding.root) {
|
||||
background = context.addRipple(context.getRoundedCornerDrawable(R.color.block_background))
|
||||
}
|
||||
|
||||
with(viewBinding.itemPendingOperationOnBehalfOfContainer) {
|
||||
background = context.getBottomRoundedCornerDrawable(R.color.block_background)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(model: PendingMultisigOperationModel) = with(viewBinding) {
|
||||
itemPendingOperationTitle.text = model.call.title
|
||||
itemPendingOperationSubtitle.setTextOrHide(model.call.subtitle)
|
||||
|
||||
itemPendingOperationChain.loadChainIcon(model.chain.icon, imageLoader)
|
||||
itemPendingOperationIcon.setIcon(model.call.icon, imageLoader)
|
||||
|
||||
itemPendingOperationProgress.text = model.progress
|
||||
itemPendingOperationAction.letOrHide(model.action) { action ->
|
||||
itemPendingOperationAction.setColoredText(action.text)
|
||||
itemPendingOperationAction.setDrawableEnd(action.icon, widthInDp = 16, paddingInDp = 4)
|
||||
}
|
||||
|
||||
itemPendingOperationPrimaryValue.setTextOrHide(model.call.primaryValue)
|
||||
itemPendingOperationTime.setTextOrHide(model.time)
|
||||
|
||||
itemPendingOperationOnBehalfOfContainer.letOrHide(model.call.onBehalfOf) { onBehalfOf ->
|
||||
itemPendingOperationOnBehalfOfAddress.text = onBehalfOf.nameOrAddress
|
||||
itemPendingOperationOnBehalfOfIcon.setImageDrawable(onBehalfOf.image)
|
||||
}
|
||||
|
||||
root.setOnClickListener { itemHandler.itemClicked(model) }
|
||||
}
|
||||
}
|
||||
|
||||
private class PendingMultisigOperationDiffCallback : BaseGroupedDiffCallback<PendingMultisigOperationHeaderModel, PendingMultisigOperationModel>(
|
||||
PendingMultisigOperationHeaderModel::class.java
|
||||
) {
|
||||
|
||||
override fun areGroupItemsTheSame(oldItem: PendingMultisigOperationHeaderModel, newItem: PendingMultisigOperationHeaderModel): Boolean {
|
||||
return oldItem.daysSinceEpoch == newItem.daysSinceEpoch
|
||||
}
|
||||
|
||||
override fun areGroupContentsTheSame(oldItem: PendingMultisigOperationHeaderModel, newItem: PendingMultisigOperationHeaderModel): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun areChildItemsTheSame(oldItem: PendingMultisigOperationModel, newItem: PendingMultisigOperationModel): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areChildContentsTheSame(oldItem: PendingMultisigOperationModel, newItem: PendingMultisigOperationModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.domain.onLoaded
|
||||
import io.novafoundation.nova.common.domain.onNotLoaded
|
||||
import io.novafoundation.nova.common.utils.insets.applyNavigationBarInsets
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.makeGone
|
||||
import io.novafoundation.nova.common.utils.makeVisible
|
||||
import io.novafoundation.nova.feature_multisig_operations.databinding.FragmentMultisigPendingOperationsBinding
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureComponent
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.model.PendingMultisigOperationModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class MultisigPendingOperationsFragment :
|
||||
BaseFragment<MultisigPendingOperationsViewModel, FragmentMultisigPendingOperationsBinding>(),
|
||||
MultisigPendingOperationsAdapter.ItemHandler {
|
||||
|
||||
override fun createBinding() = FragmentMultisigPendingOperationsBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val adapter: MultisigPendingOperationsAdapter by lazy(LazyThreadSafetyMode.NONE) { MultisigPendingOperationsAdapter(this, imageLoader) }
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.multisigPendingOperationsToolbar.applyStatusBarInsets()
|
||||
binder.multisigPendingOperationsList.applyNavigationBarInsets(consume = false)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.multisigPendingOperationsList.setHasFixedSize(true)
|
||||
binder.multisigPendingOperationsList.adapter = adapter
|
||||
|
||||
binder.multisigPendingOperationsToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<MultisigOperationsFeatureComponent>(
|
||||
requireContext(),
|
||||
MultisigOperationsFeatureApi::class.java
|
||||
)
|
||||
.multisigPendingOperations()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: MultisigPendingOperationsViewModel) {
|
||||
viewModel.pendingOperationsFlow.observe {
|
||||
it.onLoaded { data ->
|
||||
binder.multisigPendingOperationsPlaceholder.isVisible = data.isEmpty()
|
||||
binder.multisigPendingOperationsProgress.makeGone()
|
||||
binder.multisigPendingOperationsList.makeVisible()
|
||||
adapter.submitList(data)
|
||||
}.onNotLoaded {
|
||||
binder.multisigPendingOperationsProgress.makeVisible()
|
||||
binder.multisigPendingOperationsList.makeGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun itemClicked(model: PendingMultisigOperationModel) {
|
||||
viewModel.operationClicked(model)
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.list.toListWithHeaders
|
||||
import io.novafoundation.nova.common.presentation.ColoredDrawable
|
||||
import io.novafoundation.nova.common.presentation.ColoredText
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.formatting.format
|
||||
import io.novafoundation.nova.common.utils.withSafeLoading
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_multisig_operations.R
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.fromOperationId
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.model.PendingMultisigOperationHeaderModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.model.PendingMultisigOperationModel
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.model.PendingMultisigOperationModel.SigningAction
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class MultisigPendingOperationsViewModel(
|
||||
discoveryService: MultisigPendingOperationsService,
|
||||
private val router: MultisigOperationsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val multisigCallFormatter: MultisigCallFormatter,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val account = selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
|
||||
val pendingOperationsFlow = discoveryService.pendingOperations()
|
||||
.map { operations ->
|
||||
val account = account.first()
|
||||
|
||||
operations
|
||||
.sortedByDescending { it.timestamp }
|
||||
.groupBy { it.timestamp.inWholeDays }
|
||||
.toSortedMap(Comparator.reverseOrder())
|
||||
.toListWithHeaders(
|
||||
keyMapper = { day, _ -> PendingMultisigOperationHeaderModel(day) },
|
||||
valueMapper = { operation -> operation.toUi(account) }
|
||||
)
|
||||
}
|
||||
.withSafeLoading()
|
||||
.shareInBackground()
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun operationClicked(model: PendingMultisigOperationModel) {
|
||||
val operationPayload = MultisigOperationPayload.fromOperationId(model.id)
|
||||
router.openMultisigOperationDetails(MultisigOperationDetailsPayload(operationPayload))
|
||||
}
|
||||
|
||||
private suspend fun PendingMultisigOperation.toUi(selectedAccount: MetaAccount): PendingMultisigOperationModel {
|
||||
val initialOrigin = selectedAccount.requireAccountIdKeyIn(chain)
|
||||
val formattedCall = multisigCallFormatter.formatPreview(call, initialOrigin, chain)
|
||||
|
||||
return PendingMultisigOperationModel(
|
||||
id = operationId,
|
||||
chain = mapChainToUi(chain),
|
||||
action = formatAction(),
|
||||
call = formattedCall,
|
||||
progress = formatProgress(),
|
||||
time = resourceManager.formatTime(timestamp.inWholeMilliseconds)
|
||||
)
|
||||
}
|
||||
|
||||
private fun PendingMultisigOperation.formatProgress(): String {
|
||||
return resourceManager.getString(R.string.multisig_operations_progress, approvals.size.format(), threshold.format())
|
||||
}
|
||||
|
||||
private fun PendingMultisigOperation.formatAction(): SigningAction? {
|
||||
return when (userAction()) {
|
||||
is MultisigAction.CanApprove -> null
|
||||
|
||||
is MultisigAction.CanReject -> SigningAction(
|
||||
text = ColoredText(
|
||||
text = resourceManager.getText(R.string.multisig_operations_created),
|
||||
colorRes = R.color.text_secondary
|
||||
),
|
||||
icon = null
|
||||
)
|
||||
|
||||
MultisigAction.Signed -> SigningAction(
|
||||
text = ColoredText(
|
||||
text = resourceManager.getText(R.string.multisig_operations_signed),
|
||||
colorRes = R.color.text_positive
|
||||
),
|
||||
icon = ColoredDrawable(
|
||||
drawableRes = R.drawable.ic_checkmark_circle_16,
|
||||
iconColor = R.color.icon_positive
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.MultisigPendingOperationsFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
MultisigPendingOperationsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface MultisigPendingOperationsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): MultisigPendingOperationsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: MultisigPendingOperationsFragment)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.MultisigOperationsRouter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.list.MultisigPendingOperationsViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class MultisigPendingOperationsModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(MultisigPendingOperationsViewModel::class)
|
||||
fun provideViewModel(
|
||||
discoveryService: MultisigPendingOperationsService,
|
||||
router: MultisigOperationsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
multisigCallFormatter: MultisigCallFormatter,
|
||||
accountUseCase: SelectedAccountUseCase,
|
||||
): ViewModel {
|
||||
return MultisigPendingOperationsViewModel(
|
||||
discoveryService = discoveryService,
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
multisigCallFormatter = multisigCallFormatter,
|
||||
selectedAccountUseCase = accountUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): MultisigPendingOperationsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(MultisigPendingOperationsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list.model
|
||||
|
||||
class PendingMultisigOperationHeaderModel(val daysSinceEpoch: Long)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_multisig_operations.presentation.list.model
|
||||
|
||||
import io.novafoundation.nova.common.presentation.ColoredDrawable
|
||||
import io.novafoundation.nova.common.presentation.ColoredText
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallPreviewModel
|
||||
|
||||
data class PendingMultisigOperationModel(
|
||||
val id: PendingMultisigOperationId,
|
||||
val chain: ChainUi,
|
||||
val action: SigningAction?,
|
||||
val call: MultisigCallPreviewModel,
|
||||
val time: String?,
|
||||
val progress: String
|
||||
) {
|
||||
|
||||
data class SigningAction(
|
||||
val text: ColoredText,
|
||||
val icon: ColoredDrawable?
|
||||
)
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/multisigPendingOperationDetailsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:titleText="Balances.transfer" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/multisigPendingOperationProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/multisigPendingOperationDetailsToolbar">
|
||||
|
||||
<ProgressBar
|
||||
style="@style/Widget.Nova.ProgressBar.Indeterminate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/loading_transaction_details" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/multisigPendingOperationDetailsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/multisigPendingOperationDetailsToolbar">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.amount.PrimaryAmountView
|
||||
android:id="@+id/multisigPendingOperationPrimaryAmount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsNetwork"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_network" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsWallet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/account_multisig" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsBehalfOf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/multisig_on_behalf_of" />
|
||||
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:id="@+id/multisigPendingOperationDetailsCallTable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsSignatory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/common_signatory" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
android:id="@+id/multisigPendingOperationDetailsFee"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.ExpandableView
|
||||
android:id="@+id/multisigOperationSignatoriesContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_block_12"
|
||||
android:paddingTop="12dp"
|
||||
app:chevronId="@+id/multisigOperationShowHideButtonIcon"
|
||||
app:expandableId="@+id/multisigOperationSignatoriesExpandablePart"
|
||||
app:supportAnimation="false">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multisigOperationSignatoriesTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/multisigOperationShowHideButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/multisigOperationShowHideButton"
|
||||
tools:text="Signatories (1 of 4)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multisigOperationShowHideButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingEnd="4dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/multisigOperationShowHideButtonIcon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/multisigOperationShowHideButtonIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Show" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/multisigOperationShowHideButtonIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_chevron_up"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/icon_secondary" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/multisigOperationShowHideButtonIcon" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/multisigOperationSignatoriesExpandablePart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/multisigOperationShowHideButtonIcon">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/multisigOperationSignatoriesShimmering"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/item_signatory_shimmering" />
|
||||
|
||||
<include layout="@layout/item_signatory_shimmering" />
|
||||
|
||||
<include layout="@layout/item_signatory_shimmering" />
|
||||
|
||||
<include layout="@layout/item_signatory_shimmering" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/multisigOperationSignatories"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="8dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/item_multisig_signatory_account" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</io.novafoundation.nova.common.view.ExpandableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.GoNextView
|
||||
android:id="@+id/multisigPendingOperationCallDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/common_full_details"
|
||||
android:textAppearance="@style/GoNextTransactionDetailsTextAppearance"
|
||||
android:textColor="@color/button_text_accent"
|
||||
app:actionIcon="@drawable/ic_chevron_right"
|
||||
app:actionTint="@color/icon_secondary"
|
||||
app:dividerVisible="false"
|
||||
tools:background="@color/block_background" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/multisigPendingOperationDetailsEnterCallData"
|
||||
style="@style/Widget.Nova.Button.Secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/multisig_operation_details_call_data_not_found" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/multisigPendingOperationDetailsAction"
|
||||
style="@style/Widget.Nova.Button.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/common_confirm" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/multisigPendingOperationDetailsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/multisigOperationEnterCallToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:titleText="@string/enter_call_data_title" />
|
||||
|
||||
<io.novafoundation.nova.common.view.InputField
|
||||
android:id="@+id/multisigOperationEnterCallInput"
|
||||
style="@style/Widget.Nova.Input.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="top"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="2"
|
||||
app:editTextHint="@string/account_import_seed_hint"
|
||||
app:editTextMinHeight="64dp"
|
||||
app:endIconDrawable="@drawable/ic_x_clear_filled"
|
||||
app:endIconMode="clear_text" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/multisigOperationEnterCallAction"
|
||||
style="@style/Widget.Nova.Button.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
tools:text="@string/common_confirm" />
|
||||
</LinearLayout>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/multisigPendingOperationFullDetailsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:titleText="@string/transaction_details_title" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsDepositor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/common_depositor" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsDeposit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/multisig_deposit"
|
||||
app:titleIcon="@drawable/ic_info"
|
||||
app:titleIconStart="@drawable/ic_lock"
|
||||
app:titleIconStartTint="@color/icon_secondary" />
|
||||
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsCallHash"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/common_call_hash" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/multisigPendingOperationDetailsCallData"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/common_call_data" />
|
||||
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/multisig_extrinsic_details_subtitle"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multisigPendingOperationDetailsCall"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Monospace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:background="@drawable/extrinsic_details_background"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="@tools:sample/lorem[200]" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/drawable_background_image"
|
||||
android:orientation="vertical">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/multisigPendingOperationsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/blur_navigation_background"
|
||||
app:contentBackground="@android:color/transparent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:titleText="@string/multisig_pending_operations" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/multisigPendingOperationsProgress"
|
||||
style="@style/Widget.Nova.ProgressBar.Indeterminate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PlaceholderView
|
||||
android:id="@+id/multisigPendingOperationsPlaceholder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multisig_operations_placeholder"
|
||||
android:visibility="gone"
|
||||
app:image="@drawable/ic_placeholder"
|
||||
app:imageTint="@color/icon_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/multisigPendingOperationsToolbar"
|
||||
app:placeholderBackgroundStyle="no_background"
|
||||
app:showButton="false" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/multisigPendingOperationsList"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/multisigPendingOperationsToolbar"
|
||||
tools:listitem="@layout/item_multisig_pending_operation" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,189 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="4dp"
|
||||
tools:background="@color/block_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPendingOperationProgress"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Caps1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="signing (1 of 3)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_pending_operation_action"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Signed"
|
||||
tools:textColor="@color/text_positive" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemPendingOperationIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:padding="2dp"
|
||||
android:background="@drawable/bg_icon_container_on_color"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemPendingOperationOnBehalfOfContainer"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemPendingOperationProgress"
|
||||
app:layout_goneMarginBottom="20dp"
|
||||
app:tint="@color/text_secondary"
|
||||
tools:src="@drawable/ic_arrow_up" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemPendingOperationChain"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="19dp"
|
||||
android:layout_marginTop="19dp"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemPendingOperationIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/itemPendingOperationIcon"
|
||||
tools:src="@drawable/ic_polkadot_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPendingOperationTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.SubHeadline.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemPendingOperationSubtitle"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemPendingOperationIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/itemPendingOperationIcon"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Transfer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPendingOperationSubtitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/itemPendingOperationIcon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemPendingOperationTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemPendingOperationTitle"
|
||||
tools:text="To: GybH5si5nAGybH5GybH5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPendingOperationPrimaryValue"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.SubHeadline.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemPendingOperationTime"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/itemPendingOperationIcon"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="−10 DOT" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPendingOperationTime"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/itemPendingOperationIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemPendingOperationPrimaryValue"
|
||||
tools:text="18:00" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/itemPendingOperationOnBehalfOfContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:background="@color/block_background">
|
||||
|
||||
<View
|
||||
android:id="@+id/itemPendingOperationOnBehalfOfDivider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/multisig_pending_operations_on_behalf_of"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemPendingOperationOnBehalfOfIcon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/itemPendingOperationOnBehalfOfAddress"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemPendingOperationOnBehalfOfAddress"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView4"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toTopOf="@+id/itemPendingOperationOnBehalfOfAddress"
|
||||
tools:background="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPendingOperationOnBehalfOfAddress"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemPendingOperationOnBehalfOfIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Proxy Wallet" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="190dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/itemMultisigPendingOperationHeader"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="9dp"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="May 7" />
|
||||
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_primary_list_item">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemSignatoryAccountIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@color/icon_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemSignatoryAccountTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.SubHeadline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="40dp"
|
||||
android:ellipsize="middle"
|
||||
android:includeFontPadding="false"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemSignatoryAccountSubtitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemSignatoryAccountSelected"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemSignatoryAccountIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="✨👍✨ Day7 ✨👍✨" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_info"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/itemSignatoryAccountTitle"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemSignatoryAccountTitle"
|
||||
app:layout_constraintTop_toTopOf="@+id/itemSignatoryAccountTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemSignatoryAccountSubtitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="center_vertical"
|
||||
android:includeFontPadding="false"
|
||||
android:lines="1"
|
||||
android:minHeight="18dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemSignatoryAccountSelected"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemSignatoryAccountIcon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemSignatoryAccountTitle"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="$11,529.26" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemSignatoryAccountSelected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_checkmark_filled"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/icon_positive" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
android:id="@+id/signatoryShimmeringIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="@drawable/bg_shimmering_circle"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:background="@drawable/bg_shimmering"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/signatoryShimmeringIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
Reference in New Issue
Block a user