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:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_account_api.data.cloudBackup
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
fun CloudBackup.WalletPublicInfo.Type.toMetaAccountType(): LightMetaAccount.Type {
return when (this) {
CloudBackup.WalletPublicInfo.Type.SECRETS -> LightMetaAccount.Type.SECRETS
CloudBackup.WalletPublicInfo.Type.WATCH_ONLY -> LightMetaAccount.Type.WATCH_ONLY
CloudBackup.WalletPublicInfo.Type.PARITY_SIGNER -> LightMetaAccount.Type.PARITY_SIGNER
CloudBackup.WalletPublicInfo.Type.LEDGER -> LightMetaAccount.Type.LEDGER_LEGACY
CloudBackup.WalletPublicInfo.Type.POLKADOT_VAULT -> LightMetaAccount.Type.POLKADOT_VAULT
CloudBackup.WalletPublicInfo.Type.LEDGER_GENERIC -> LightMetaAccount.Type.LEDGER
}
}
@@ -0,0 +1,75 @@
package io.novafoundation.nova.feature_account_api.data.cloudBackup
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.CloudBackupDiff
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.localVsCloudDiff
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.BackupDiffStrategyFactory
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CannotApplyNonDestructiveDiff
import io.novasama.substrate_sdk_android.scale.EncodableStruct
const val CLOUD_BACKUP_APPLY_SOURCE = "CLOUD_BACKUP_APPLY_SOURCE"
interface LocalAccountsCloudBackupFacade {
/**
* Constructs full backup instance, including sensitive information
* Should only be used when full backup instance is needed, for example when writing backup to cloud. Otherwise use [publicBackupInfoFromLocalSnapshot]
*
* Important note: Should only be called as the result of direct user interaction!
* We don't want to exposure secrets to RAM until user explicitly directs app to do so
*/
suspend fun fullBackupInfoFromLocalSnapshot(): CloudBackup
/**
* Constructs partial backup instance, including only the public information (addresses, metadata e.t.c)
*
* Can be used without direct user interaction (e.g. in background) to compare backup states between local and remote sources
*/
suspend fun publicBackupInfoFromLocalSnapshot(): CloudBackup.PublicData
/**
* Creates a backup from external input. Useful for creating initial backup
*/
suspend fun constructCloudBackupForFirstWallet(
metaAccount: MetaAccountLocal,
baseSecrets: EncodableStruct<MetaAccountSecrets>,
): CloudBackup
/**
* Check if it is possible to apply given [diff] to local state in non-destructive manner
* In other words, whether it is possible to apply backup without notifying the user
*/
suspend fun canPerformNonDestructiveApply(diff: CloudBackupDiff): Boolean
/**
* Applies cloud version of the backup to the local state.
* This is a destructive action as may overwrite or delete secrets stored in the app
*
* Important note: Should only be called as the result of direct user interaction!
*/
suspend fun applyBackupDiff(diff: CloudBackupDiff, cloudVersion: CloudBackup)
}
/**
* Attempts to apply cloud backup version to current local application state in non-destructive manner
* Will do nothing if it is not possible to apply changes in non-destructive manner
*
* @return whether the attempt succeeded
*/
suspend fun LocalAccountsCloudBackupFacade.applyNonDestructiveCloudVersionOrThrow(
cloudVersion: CloudBackup,
diffStrategy: BackupDiffStrategyFactory
): CloudBackupDiff {
val localSnapshot = publicBackupInfoFromLocalSnapshot()
val diff = localSnapshot.localVsCloudDiff(cloudVersion.publicData, diffStrategy)
return if (canPerformNonDestructiveApply(diff)) {
applyBackupDiff(diff, cloudVersion)
diff
} else {
throw CannotApplyNonDestructiveDiff(diff, cloudVersion)
}
}
@@ -0,0 +1,29 @@
@file:Suppress("RedundantUnitExpression")
package io.novafoundation.nova.feature_account_api.data.conversion.assethub
import io.novafoundation.nova.common.data.network.runtime.binding.bindPair
import io.novafoundation.nova.common.utils.assetConversionOrNull
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindMultiLocation
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
@JvmInline
value class AssetConversionApi(override val module: Module) : QueryableModule
context(StorageQueryContext)
val RuntimeMetadata.assetConversionOrNull: AssetConversionApi?
get() = assetConversionOrNull()?.let(::AssetConversionApi)
context(StorageQueryContext)
val AssetConversionApi.pools: QueryableStorageEntry1<Pair<RelativeMultiLocation, RelativeMultiLocation>, Unit>
get() = storage1(
name = "Pools",
binding = { _, _ -> Unit },
keyBinding = { bindPair(it, ::bindMultiLocation, ::bindMultiLocation) }
)
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_account_api.data.derivationPath
import io.novasama.substrate_sdk_android.encrypt.junction.BIP32JunctionDecoder
import io.novasama.substrate_sdk_android.encrypt.junction.JunctionDecoder
import io.novasama.substrate_sdk_android.encrypt.junction.SubstrateJunctionDecoder
object DerivationPathDecoder {
@Throws
fun decodeEthereumDerivationPath(derivationPath: String?): JunctionDecoder.DecodeResult? {
if (derivationPath.isNullOrEmpty()) return null
return BIP32JunctionDecoder.decode(derivationPath)
}
@Throws
fun decodeSubstrateDerivationPath(derivationPath: String?): JunctionDecoder.DecodeResult? {
if (derivationPath.isNullOrEmpty()) return null
return SubstrateJunctionDecoder.decode(derivationPath)
}
fun isEthereumDerivationPathValid(derivationPath: String?): Boolean {
return try {
decodeEthereumDerivationPath(derivationPath)
true
} catch (e: Exception) {
false
}
}
fun isSubstrateDerivationPathValid(derivationPath: String?): Boolean {
return try {
decodeSubstrateDerivationPath(derivationPath)
true
} catch (e: Exception) {
false
}
}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.data.ethereum.transaction
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
class EthereumTransactionExecution(
val extrinsicHash: String,
val blockHash: BlockHash,
val submissionHierarchy: SubmissionHierarchy
)
@@ -0,0 +1,36 @@
package io.novafoundation.nova.feature_account_api.data.ethereum.transaction
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import org.web3j.tx.gas.DefaultGasProvider
import java.math.BigInteger
typealias EvmTransactionBuilding = EvmTransactionBuilder.() -> Unit
interface EvmTransactionService {
suspend fun calculateFee(
chainId: ChainId,
origin: TransactionOrigin,
fallbackGasLimit: BigInteger = DefaultGasProvider.GAS_LIMIT,
building: EvmTransactionBuilding,
): Fee
suspend fun transact(
chainId: ChainId,
presetFee: Fee?,
origin: TransactionOrigin,
fallbackGasLimit: BigInteger = DefaultGasProvider.GAS_LIMIT,
building: EvmTransactionBuilding,
): Result<ExtrinsicSubmission>
suspend fun transactAndAwaitExecution(
chainId: ChainId,
presetFee: Fee?,
origin: TransactionOrigin,
fallbackGasLimit: BigInteger,
building: EvmTransactionBuilding
): Result<EthereumTransactionExecution>
}
@@ -0,0 +1,3 @@
package io.novafoundation.nova.feature_account_api.data.ethereum.transaction
typealias TransactionHash = String
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_account_api.data.ethereum.transaction
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novasama.substrate_sdk_android.runtime.AccountId
sealed class TransactionOrigin {
data object SelectedWallet : TransactionOrigin()
class WalletWithAccount(val accountId: AccountId) : TransactionOrigin()
class Wallet(val metaAccount: MetaAccount) : TransactionOrigin()
class WalletWithId(val metaId: Long) : TransactionOrigin()
}
fun AccountId.intoOrigin(): TransactionOrigin = TransactionOrigin.WalletWithAccount(this)
fun MetaAccount.intoOrigin(): TransactionOrigin = TransactionOrigin.Wallet(this)
@@ -0,0 +1,150 @@
package io.novafoundation.nova.feature_account_api.data.events
import io.novafoundation.nova.common.utils.bus.EventBus
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus.Event
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
interface MetaAccountChangesEventBus : EventBus<Event> {
sealed interface Event : EventBus.Event {
data class BatchUpdate(val updates: Collection<Event>) : Event
data class AccountAdded(override val metaId: Long, override val metaAccountType: LightMetaAccount.Type) : Event, SingleUpdateEvent
data class AccountStructureChanged(override val metaId: Long, override val metaAccountType: LightMetaAccount.Type) : Event, SingleUpdateEvent
data class AccountRemoved(override val metaId: Long, override val metaAccountType: LightMetaAccount.Type) : Event, SingleUpdateEvent
data class AccountNameChanged(override val metaId: Long, override val metaAccountType: LightMetaAccount.Type) : Event, SingleUpdateEvent
}
interface SingleUpdateEvent {
val metaId: Long
val metaAccountType: LightMetaAccount.Type
}
interface EventVisitor {
fun visitAccountAdded(added: Event.AccountAdded) {}
fun visitAccountStructureChanged(structureChanged: Event.AccountStructureChanged) {}
fun visitAccountNameChanged(accountNameChanged: Event.AccountNameChanged) {}
fun visitAccountRemoved(accountRemoved: Event.AccountRemoved) {}
}
}
inline fun buildChangesEvent(builder: MutableList<Event>.() -> Unit): Event? {
val allEvents = buildList(builder)
return allEvents.combineBusEvents()
}
fun List<Event>.combineBusEvents(): Event? {
return when (size) {
0 -> null
1 -> single()
else -> Event.BatchUpdate(this)
}
}
fun Event.visit(visitor: MetaAccountChangesEventBus.EventVisitor) {
when (this) {
is Event.AccountAdded -> visitor.visitAccountAdded(this)
is Event.AccountNameChanged -> visitor.visitAccountNameChanged(this)
is Event.AccountRemoved -> visitor.visitAccountRemoved(this)
is Event.AccountStructureChanged -> visitor.visitAccountStructureChanged(this)
is Event.BatchUpdate -> updates.onEach { it.visit(visitor) }
}
}
typealias EventBusEventCollator<E, T> = (E) -> T?
fun <T> Event.collect(
onAdd: EventBusEventCollator<Event.AccountAdded, T>? = null,
onStructureChanged: EventBusEventCollator<Event.AccountStructureChanged, T>? = null,
onNameChanged: EventBusEventCollator<Event.AccountNameChanged, T>? = null,
onRemoved: EventBusEventCollator<Event.AccountRemoved, T>? = null,
): List<T> {
val result = mutableListOf<T>()
visit(object : MetaAccountChangesEventBus.EventVisitor {
override fun visitAccountAdded(added: Event.AccountAdded) {
onAdd?.invoke(added)?.let(result::add)
}
override fun visitAccountStructureChanged(structureChanged: Event.AccountStructureChanged) {
onStructureChanged?.invoke(structureChanged)?.let(result::add)
}
override fun visitAccountNameChanged(accountNameChanged: Event.AccountNameChanged) {
onNameChanged?.invoke(accountNameChanged)?.let(result::add)
}
override fun visitAccountRemoved(accountRemoved: Event.AccountRemoved) {
onRemoved?.invoke(accountRemoved)?.let(result::add)
}
})
return result
}
typealias SingleAccountEventVisitor<T> = (T) -> Unit
fun Event.visit(
onAdd: SingleAccountEventVisitor<Event.AccountAdded>? = null,
onStructureChanged: SingleAccountEventVisitor<Event.AccountStructureChanged>? = null,
onNameChanged: SingleAccountEventVisitor<Event.AccountNameChanged>? = null,
onRemoved: SingleAccountEventVisitor<Event.AccountRemoved>? = null,
) {
visit(object : MetaAccountChangesEventBus.EventVisitor {
override fun visitAccountAdded(added: Event.AccountAdded) {
onAdd?.invoke(added)
}
override fun visitAccountStructureChanged(structureChanged: Event.AccountStructureChanged) {
onStructureChanged?.invoke(structureChanged)
}
override fun visitAccountNameChanged(accountNameChanged: Event.AccountNameChanged) {
onNameChanged?.invoke(accountNameChanged)
}
override fun visitAccountRemoved(accountRemoved: Event.AccountRemoved) {
onRemoved?.invoke(accountRemoved)
}
})
}
fun Event.checkIncludes(
checkAdd: Boolean = false,
checkStructureChange: Boolean = false,
checkNameChange: Boolean = false,
checkAccountRemoved: Boolean = false
): Boolean {
var includes = false
val updateClosure: SingleAccountEventVisitor<Any> = {
includes = true
}
visit(
onAdd = updateClosure.takeIf { checkAdd },
onStructureChanged = updateClosure.takeIf { checkStructureChange },
onNameChanged = updateClosure.takeIf { checkNameChange },
onRemoved = updateClosure.takeIf { checkAccountRemoved }
)
return includes
}
fun Event.allAffectedMetaAccountTypes(): List<LightMetaAccount.Type> {
return collect(
onAdd = { it.metaAccountType },
onRemoved = { it.metaAccountType },
onNameChanged = { it.metaAccountType },
onStructureChanged = { it.metaAccountType }
)
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.data.externalAccounts
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
interface ExternalAccountsSyncService {
fun syncOnAccountChange(event: MetaAccountChangesEventBus.Event, changeSource: String?)
fun sync()
}
@@ -0,0 +1,114 @@
package io.novafoundation.nova.feature_account_api.data.extrinsic
import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse
import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.watch.ExtrinsicWatchResult
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
typealias FormExtrinsicWithOrigin = suspend ExtrinsicBuilder.(context: ExtrinsicBuildingContext) -> Unit
typealias FormMultiExtrinsicWithOrigin = suspend CallBuilder.(context: ExtrinsicBuildingContext) -> Unit
class ExtrinsicSubmission(
val hash: String,
val submissionOrigin: SubmissionOrigin,
val callExecutionType: CallExecutionType,
val submissionHierarchy: SubmissionHierarchy
)
class ExtrinsicBuildingContext(
val submissionOrigin: SubmissionOrigin,
val signer: NovaSigner,
val chain: Chain
)
private val DEFAULT_BATCH_MODE = BatchMode.BATCH_ALL
interface ExtrinsicService {
interface Factory {
fun create(feeConfig: FeePaymentConfig): ExtrinsicService
}
class SubmissionOptions(
val feePaymentCurrency: FeePaymentCurrency = FeePaymentCurrency.Native,
val batchMode: BatchMode = DEFAULT_BATCH_MODE,
)
class FeePaymentConfig(
val coroutineScope: CoroutineScope,
/**
* Specify to use it instead of default [FeePaymentProviderRegistry] to perform fee computations
*/
val customFeePaymentRegistry: FeePaymentProviderRegistry? = null,
)
suspend fun submitExtrinsic(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormExtrinsicWithOrigin
): Result<ExtrinsicSubmission>
suspend fun submitAndWatchExtrinsic(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormExtrinsicWithOrigin
): Result<Flow<ExtrinsicWatchResult<ExtrinsicStatus>>>
suspend fun submitExtrinsicAndAwaitExecution(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormExtrinsicWithOrigin
): Result<ExtrinsicExecutionResult>
suspend fun submitMultiExtrinsicAwaitingInclusion(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormMultiExtrinsicWithOrigin
): RetriableMultiResult<ExtrinsicWatchResult<ExtrinsicStatus.InBlock>>
suspend fun paymentInfo(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormExtrinsicWithOrigin
): FeeResponse
suspend fun estimateFee(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormExtrinsicWithOrigin
): Fee
suspend fun estimateMultiFee(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions = SubmissionOptions(),
formExtrinsic: FormMultiExtrinsicWithOrigin
): Fee
suspend fun estimateFee(
chain: Chain,
extrinsic: String,
usedSigner: NovaSigner,
): Fee
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_account_api.data.extrinsic
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService.FeePaymentConfig
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.watch.ExtrinsicWatchResult
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.watch.mapWithStatus
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
suspend fun Result<Flow<ExtrinsicWatchResult<ExtrinsicStatus>>>.awaitInBlock(): Result<ExtrinsicWatchResult<ExtrinsicStatus.InBlock>> =
mapCatching { watchResult ->
watchResult.filter { it.status is ExtrinsicStatus.InBlock }
.map { it.mapWithStatus<ExtrinsicStatus.InBlock>() }
.first()
}
suspend inline fun <reified T : ExtrinsicStatus> Flow<ExtrinsicWatchResult<*>>.awaitStatus(): ExtrinsicWatchResult<T> {
return filterStatus<T>().first()
}
inline fun <reified T : ExtrinsicStatus> Flow<ExtrinsicWatchResult<*>>.filterStatus(): Flow<ExtrinsicWatchResult<T>> {
return filter { it.status is T }
.map { it.mapWithStatus<T>() }
}
fun ExtrinsicService.Factory.createDefault(coroutineScope: CoroutineScope): ExtrinsicService {
return create(FeePaymentConfig(coroutineScope))
}
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_account_api.data.extrinsic
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
typealias SplitCalls = List<List<GenericCall.Instance>>
interface ExtrinsicSplitter {
suspend fun split(signer: NovaSigner, callBuilder: CallBuilder, chain: Chain): SplitCalls
suspend fun estimateCallWeight(signer: NovaSigner, call: GenericCall.Instance, chain: Chain): WeightV2
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_account_api.data.extrinsic
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer
data class SubmissionOrigin(
/**
* Account on which behalf the operation will be executed
*/
val executingAccount: AccountId,
/**
* Account that will sign and submit transaction
* It might differ from [executingAccount] if [Signer] modified the origin.
* For example in the case of Proxied wallet [executingAccount] is proxied and [signingAccount] is proxy
*/
val signingAccount: AccountId
) {
companion object {
fun singleOrigin(origin: AccountId) = SubmissionOrigin(origin, origin)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SubmissionOrigin
if (!executingAccount.contentEquals(other.executingAccount)) return false
return signingAccount.contentEquals(other.signingAccount)
}
override fun hashCode(): Int {
var result = executingAccount.contentHashCode()
result = 31 * result + signingAccount.contentHashCode()
return result
}
}
@@ -0,0 +1,51 @@
package io.novafoundation.nova.feature_account_api.data.extrinsic.execution
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
import io.novafoundation.nova.common.data.network.runtime.binding.DispatchError
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
data class ExtrinsicExecutionResult(
val extrinsicHash: String,
val blockHash: BlockHash,
val outcome: ExtrinsicDispatch,
val submissionHierarchy: SubmissionHierarchy
)
sealed interface ExtrinsicDispatch {
data class Ok(val emittedEvents: List<GenericEvent.Instance>) : ExtrinsicDispatch
data class Failed(val error: DispatchError) : ExtrinsicDispatch
object Unknown : ExtrinsicDispatch
}
fun ExtrinsicExecutionResult.requireOk(): ExtrinsicExecutionResult {
return when (outcome) {
is ExtrinsicDispatch.Failed -> throw outcome.error
is ExtrinsicDispatch.Ok -> this
ExtrinsicDispatch.Unknown -> throw IllegalArgumentException("Unknown extrinsic execution result")
}
}
fun Result<ExtrinsicExecutionResult>.requireOk(): Result<ExtrinsicExecutionResult> {
return mapCatching {
it.requireOk()
}
}
fun ExtrinsicExecutionResult.requireOutcomeOk(): ExtrinsicDispatch.Ok {
return requireOk().outcome as ExtrinsicDispatch.Ok
}
fun ExtrinsicDispatch.isOk(): Boolean {
return this is ExtrinsicDispatch.Ok
}
fun ExtrinsicDispatch.isModuleError(moduleName: String, errorName: String): Boolean {
return this is ExtrinsicDispatch.Failed &&
error is DispatchError.Module &&
error.module.name == moduleName &&
error.error.name == errorName
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_account_api.data.extrinsic.execution.watch
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
class ExtrinsicWatchResult<T : ExtrinsicStatus>(
val status: T,
val submissionHierarchy: SubmissionHierarchy
)
fun List<ExtrinsicWatchResult<*>>.submissionHierarchy() = first().submissionHierarchy
inline fun <reified T : ExtrinsicStatus> ExtrinsicWatchResult<*>.mapWithStatus() = ExtrinsicWatchResult(status as T, submissionHierarchy)
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_account_api.data.fee
import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
class DefaultFastLookupCustomFeeCapability : FastLookupCustomFeeCapability {
override val nonUtilityFeeCapableTokens: Set<ChainAssetId> = emptySet()
}
@@ -0,0 +1,41 @@
package io.novafoundation.nova.feature_account_api.data.fee
import android.util.Log
import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import kotlinx.coroutines.CoroutineScope
interface FeePayment {
suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder)
suspend fun convertNativeFee(nativeFee: Fee): Fee
}
interface FeePaymentProvider {
val chain: Chain
suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment
suspend fun detectFeePaymentFromExtrinsic(extrinsic: SendableExtrinsic): FeePayment
suspend fun canPayFee(feePaymentCurrency: FeePaymentCurrency): Result<Boolean>
suspend fun fastLookupCustomFeeCapability(): Result<FastLookupCustomFeeCapability>
}
interface FeePaymentProviderRegistry {
suspend fun providerFor(chainId: ChainId): FeePaymentProvider
}
suspend fun FeePaymentProvider.fastLookupCustomFeeCapabilityOrDefault(): FastLookupCustomFeeCapability {
return fastLookupCustomFeeCapability()
.onFailure { Log.e("FeePaymentProvider", "Failed to construct fast custom fee lookup for chain ${chain.name}", it) }
.getOrElse { DefaultFastLookupCustomFeeCapability() }
}
@@ -0,0 +1,63 @@
package io.novafoundation.nova.feature_account_api.data.fee
import io.novafoundation.nova.runtime.ext.fullId
import io.novafoundation.nova.runtime.ext.isCommissionAsset
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
sealed interface FeePaymentCurrency {
/**
* Use native currency of the chain to pay the fee
*/
object Native : FeePaymentCurrency {
override fun toString(): String {
return "Native"
}
}
/**
* Request to use a specific [asset] for payment fees
* This does not guarantee that the exact [asset] will be used for fee payments,
* for example if the chain doesn't support paying fees in asset. In that case it will fall-back to using [FeePaymentCurrency.Native]
*
* The actual asset used to pay fees will be available in [Fee.asset]
*/
class Asset private constructor(val asset: Chain.Asset) : FeePaymentCurrency {
companion object {
fun Chain.Asset.toFeePaymentCurrency(): FeePaymentCurrency {
return when {
isCommissionAsset -> Native
else -> Asset(this)
}
}
}
override fun equals(other: Any?): Boolean {
if (other !is Asset) return false
return asset.fullId == other.asset.fullId
}
override fun hashCode(): Int {
return asset.hashCode()
}
override fun toString(): String {
return "Asset(${asset.symbol})"
}
}
}
fun FeePaymentCurrency.toChainAsset(chain: Chain): Chain.Asset {
return toChainAsset(chain.utilityAsset)
}
fun FeePaymentCurrency.toChainAsset(chainUtilityAsset: Chain.Asset): Chain.Asset {
return when (this) {
is FeePaymentCurrency.Asset -> asset
FeePaymentCurrency.Native -> chainUtilityAsset
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_account_api.data.fee.capability
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
interface FastLookupCustomFeeCapability {
val nonUtilityFeeCapableTokens: Set<ChainAssetId>
fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean {
return chainAssetId in nonUtilityFeeCapableTokens
}
}
interface CustomFeeCapabilityFacade {
suspend fun canPayFeeInCurrency(currency: FeePaymentCurrency): Boolean
/**
* Whether fee payment in custom assets is not possible at all in the current environment
* This check is also accounted for internally in [canPayFeeInNonUtilityToken]
* but can be used separately for optimizing bulk checks
*/
suspend fun hasGlobalFeePaymentRestrictions(): Boolean
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_api.data.fee.chains
import io.novafoundation.nova.feature_account_api.data.fee.FeePayment
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider
import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.CoroutineScope
abstract class CustomOrNativeFeePaymentProvider : FeePaymentProvider {
protected abstract suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment
protected abstract suspend fun canPayFeeInNonUtilityToken(customFeeAsset: Chain.Asset): Result<Boolean>
final override suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment {
return when (feePaymentCurrency) {
is FeePaymentCurrency.Asset -> feePaymentFor(feePaymentCurrency.asset, coroutineScope)
FeePaymentCurrency.Native -> NativeFeePayment()
}
}
final override suspend fun canPayFee(feePaymentCurrency: FeePaymentCurrency): Result<Boolean> {
return when (feePaymentCurrency) {
is FeePaymentCurrency.Asset -> canPayFeeInNonUtilityToken(feePaymentCurrency.asset)
FeePaymentCurrency.Native -> Result.success(true)
}
}
}
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_account_api.data.fee.types
import io.novafoundation.nova.feature_account_api.data.fee.FeePayment
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
class NativeFeePayment : FeePayment {
override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) {
// no modifications needed
}
override suspend fun convertNativeFee(nativeFee: Fee): Fee {
return nativeFee
}
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_account_api.data.fee.types.assetHub
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.cast
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.transactionExtensionOrNull
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindMultiLocation
import io.novafoundation.nova.runtime.extrinsic.extensions.ChargeAssetTxPayment
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.findExplicitOrNull
fun Extrinsic.Instance.findChargeAssetTxPayment(): ChargeAssetTxPaymentValue? {
val value = findExplicitOrNull(ChargeAssetTxPayment.ID) ?: return null
return ChargeAssetTxPaymentValue.bind(value)
}
fun RuntimeSnapshot.decodeCustomTxPaymentId(assetIdHex: String): Any? {
val chargeAssetTxPaymentType = metadata.extrinsic.transactionExtensionOrNull(ChargeAssetTxPayment.ID) ?: return null
val type = chargeAssetTxPaymentType.includedInExtrinsic!!
val assetIdType = type.cast<Struct>().get<Type<*>>("assetId")!!
return assetIdType.fromHex(this, assetIdHex)
}
class ChargeAssetTxPaymentValue(
val tip: BalanceOf,
val assetId: RelativeMultiLocation?
) {
companion object {
fun bind(decoded: Any?): ChargeAssetTxPaymentValue {
val asStruct = decoded.castToStruct()
return ChargeAssetTxPaymentValue(
tip = bindNumber(asStruct["tip"]),
assetId = asStruct.get<Any?>("assetId")?.let(::bindMultiLocation)
)
}
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_account_api.data.fee.types.hydra
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import java.math.BigInteger
interface HydrationFeeInjector {
class SetFeesMode(
val setMode: SetMode,
val resetMode: ResetMode
)
sealed class SetMode {
/**
* Always sets the fee to the required token, regardless of whether fees are already in the needed state or not
*/
object Always : SetMode()
/**
* Sets the fee token to the required one only the current fee payment asset is different
*/
class Lazy(val currentlySetFeeAsset: BigInteger) : SetMode()
}
sealed class ResetMode {
/**
* Always resets the fee to the native token, regardless of whether fees are already in the needed state or not
*/
object ToNative : ResetMode()
/**
* Resets the fee to the native one only the current fee payment asset is different
*/
class ToNativeLazily(val feeAssetBeforeTransaction: BigInteger) : ResetMode()
}
suspend fun setFees(
extrinsicBuilder: ExtrinsicBuilder,
paymentAsset: Chain.Asset,
mode: SetFeesMode,
)
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_account_api.data.mappers
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
fun mapChainToUi(chain: Chain): ChainUi = with(chain) {
ChainUi(
id = id,
name = name,
icon = icon,
)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_api.data.mappers
import io.novafoundation.nova.core.model.Network
import io.novafoundation.nova.core.model.Node
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
fun stubNetwork(chainId: ChainId): Network {
val networkType = Node.NetworkType.findByGenesis(chainId) ?: Node.NetworkType.POLKADOT
return Network(networkType)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_api.data.model
import io.novafoundation.nova.common.address.AccountIdKey
@Deprecated("Use AccountIdKeyMap instead")
typealias AccountIdMap<V> = Map<String, V>
@Deprecated("Use AccountIdKeyMap instead")
typealias AccountAddressMap<V> = Map<String, V>
typealias AccountIdKeyMap<V> = Map<AccountIdKey, V>
@@ -0,0 +1,113 @@
package io.novafoundation.nova.feature_account_api.data.model
import io.novafoundation.nova.common.utils.amountFromPlanks
import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin
import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.maxAction.MaxAvailableDeduction
import io.novafoundation.nova.runtime.ext.fullId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import java.math.BigDecimal
import java.math.BigInteger
// TODO rename FeeBase -> Fee and use SubmissionFee everywhere Fee is currently used
typealias Fee = SubmissionFee
interface SubmissionFee : FeeBase, MaxAvailableDeduction {
companion object
/**
* Information about origin that is supposed to send the transaction fee was calculated against
*/
val submissionOrigin: SubmissionOrigin
/**
* Submission fee deducts fee amount from max balance only when executing account pays fees
* When signing account is different from executing one, executing account balance remains unaffected by submission fee payment
*/
override fun maxAmountDeductionFor(amountAsset: Chain.Asset): BigInteger {
return getAmountByExecutingAccount(amountAsset)
}
}
val SubmissionFee.submissionFeesPayer: AccountId
get() = submissionOrigin.signingAccount
/**
* Fee that doesn't have a particular origin
* For example, fees paid during cross chain transfers do not have a specific account that pays them
*/
interface FeeBase {
val amount: BigInteger
val asset: Chain.Asset
}
val FeeBase.decimalAmount: BigDecimal
get() = amount.amountFromPlanks(asset.precision)
data class EvmFee(
val gasLimit: BigInteger,
val gasPrice: BigInteger,
override val submissionOrigin: SubmissionOrigin,
override val asset: Chain.Asset
) : Fee {
override val amount = gasLimit * gasPrice
}
class SubstrateFee(
override val amount: BigInteger,
override val submissionOrigin: SubmissionOrigin,
override val asset: Chain.Asset
) : Fee
class SubstrateFeeBase(
override val amount: BigInteger,
override val asset: Chain.Asset
) : FeeBase
val Fee.amountByExecutingAccount: BigInteger
get() = getAmount(asset, submissionOrigin.executingAccount)
val Fee.decimalAmountByExecutingAccount: BigDecimal
get() = amountByExecutingAccount.amountFromPlanks(asset.precision)
fun FeeBase.addPlanks(extraPlanks: BigInteger): FeeBase {
return SubstrateFeeBase(amount + extraPlanks, asset)
}
fun List<FeeBase>.totalAmount(chainAsset: Chain.Asset): BigInteger {
return sumOf { it.getAmount(chainAsset) }
}
fun List<SubmissionFee>.totalAmount(chainAsset: Chain.Asset, origin: AccountId): BigInteger {
return sumOf { it.getAmount(chainAsset, origin) }
}
fun List<FeeBase>.totalPlanksEnsuringAsset(requireAsset: Chain.Asset): BigInteger {
return sumOf {
require(it.asset.fullId == requireAsset.fullId) {
"Fees contain fee in different assets: ${it.asset.fullId}"
}
it.amount
}
}
fun SubmissionFee.getAmount(chainAsset: Chain.Asset, origin: AccountId): BigInteger {
return if (asset.fullId == chainAsset.fullId && submissionFeesPayer.contentEquals(origin)) {
amount
} else {
BigInteger.ZERO
}
}
fun SubmissionFee.getAmountByExecutingAccount(chainAsset: Chain.Asset): BigInteger {
return getAmount(chainAsset, submissionOrigin.executingAccount)
}
fun FeeBase.getAmount(expectedAsset: Chain.Asset): BigInteger {
return if (expectedAsset.fullId == asset.fullId) amount else BigInteger.ZERO
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_account_api.data.model
import io.novasama.substrate_sdk_android.runtime.AccountId
interface OnChainIdentity {
val display: String?
val legal: String?
val web: String?
val matrix: String?
val email: String?
val pgpFingerprint: String?
val image: String?
val twitter: String?
}
class RootIdentity(
override val display: String?,
override val legal: String?,
override val web: String?,
override val matrix: String?,
override val email: String?,
override val pgpFingerprint: String?,
override val image: String?,
override val twitter: String?,
) : OnChainIdentity
class ChildIdentity(
val childName: String?,
val parentIdentity: OnChainIdentity,
) : OnChainIdentity by parentIdentity {
override val display: String = "${parentIdentity.display} / ${childName.orEmpty()}"
}
class SuperOf(
val parentId: AccountId,
val childName: String?,
)
@@ -0,0 +1,60 @@
package io.novafoundation.nova.feature_account_api.data.multisig
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.common.utils.composeCall
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigTimePoint
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
fun RuntimeSnapshot.composeMultisigAsMulti(
multisigMetaAccount: MultisigMetaAccount,
maybeTimePoint: MultisigTimePoint?,
call: GenericCall.Instance,
maxWeight: WeightV2
): GenericCall.Instance {
return composeCall(
moduleName = Modules.MULTISIG,
callName = "as_multi",
arguments = mapOf(
"threshold" to multisigMetaAccount.threshold.toBigInteger(),
"other_signatories" to multisigMetaAccount.otherSignatories.map { it.value },
"maybe_timepoint" to maybeTimePoint?.toEncodableInstance(),
"call" to call,
"max_weight" to maxWeight.toEncodableInstance()
)
)
}
fun RuntimeSnapshot.composeMultisigAsMultiThreshold1(
multisigMetaAccount: MultisigMetaAccount,
call: GenericCall.Instance,
): GenericCall.Instance {
return composeCall(
moduleName = Modules.MULTISIG,
callName = "as_multi_threshold_1",
arguments = mapOf(
"other_signatories" to multisigMetaAccount.otherSignatories.map { it.value },
"call" to call,
)
)
}
fun RuntimeSnapshot.composeMultisigCancelAsMulti(
multisigMetaAccount: MultisigMetaAccount,
maybeTimePoint: MultisigTimePoint,
callHash: CallHash,
): GenericCall.Instance {
return composeCall(
moduleName = Modules.MULTISIG,
callName = "cancel_as_multi",
arguments = mapOf(
"threshold" to multisigMetaAccount.threshold.toBigInteger(),
"other_signatories" to multisigMetaAccount.otherSignatories.map { it.value },
"timepoint" to maybeTimePoint.toEncodableInstance(),
"call_hash" to callHash.value
)
)
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.data.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface MultisigDetailsRepository {
suspend fun hasMultisigOperation(
chain: Chain,
accountIdKey: AccountIdKey,
callHash: CallHash
): Boolean
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_account_api.data.multisig
import io.novafoundation.nova.common.data.memory.ComputationalScope
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
import kotlinx.coroutines.flow.Flow
interface MultisigPendingOperationsService {
context(ComputationalScope)
fun performMultisigOperationsSync(): Flow<Unit>
context(ComputationalScope)
fun pendingOperationsCountFlow(): Flow<Int>
context(ComputationalScope)
suspend fun getPendingOperationsCount(): Int
context(ComputationalScope)
fun pendingOperations(): Flow<List<PendingMultisigOperation>>
context(ComputationalScope)
fun pendingOperationFlow(id: PendingMultisigOperationId): Flow<PendingMultisigOperation?>
context(ComputationalScope)
suspend fun pendingOperation(id: PendingMultisigOperationId): PendingMultisigOperation?
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.data.multisig.model
sealed class MultisigAction {
data object Signed : MultisigAction()
data object CanReject : MultisigAction()
data class CanApprove(val isFinalApproval: Boolean) : MultisigAction()
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_account_api.data.multisig.model
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.bindInt
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.scale.ToDynamicScaleInstance
import io.novafoundation.nova.common.utils.structOf
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
class MultisigTimePoint(
val height: BlockNumber,
val extrinsicIndex: Int
) : ToDynamicScaleInstance {
companion object {
fun bind(decoded: Any?): MultisigTimePoint {
val asStruct = decoded.castToStruct()
return MultisigTimePoint(
height = bindBlockNumber(asStruct["height"]),
extrinsicIndex = bindInt(asStruct["index"])
)
}
}
override fun toEncodableInstance(): Struct.Instance {
return structOf(
"height" to height,
"index" to extrinsicIndex.toBigInteger()
)
}
}
@@ -0,0 +1,80 @@
package io.novafoundation.nova.feature_account_api.data.multisig.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.toHex
import io.novafoundation.nova.common.utils.Identifiable
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import java.math.BigInteger
import kotlin.time.Duration
class PendingMultisigOperation(
val multisigMetaId: Long,
val call: GenericCall.Instance?,
val callHash: CallHash,
val chain: Chain,
val timePoint: MultisigTimePoint,
val approvals: List<AccountIdKey>,
val depositor: AccountIdKey,
val deposit: BigInteger,
val signatoryAccountId: AccountIdKey,
val signatoryMetaId: Long,
val threshold: Int,
val timestamp: Duration,
) : Identifiable {
val operationId = PendingMultisigOperationId(multisigMetaId, chain.id, callHash.toHex())
override val identifier: String = operationId.identifier()
override fun toString(): String {
val callFormatted = if (call != null) {
"${call.module.name}.${call.function.name}"
} else {
callHash.toHex()
}
return "Call: $callFormatted, Chain: ${chain.name}, Approvals: ${approvals.size}/$threshold, User action: ${userAction()}"
}
companion object
}
data class PendingMultisigOperationId(
val metaId: Long,
val chainId: ChainId,
val callHash: String,
) {
companion object;
}
fun PendingMultisigOperation.userAction(): MultisigAction {
return when (signatoryAccountId) {
depositor -> MultisigAction.CanReject
!in approvals -> MultisigAction.CanApprove(
isFinalApproval = approvals.size == threshold - 1
)
else -> MultisigAction.Signed
}
}
fun PendingMultisigOperationId.identifier() = toString()
/**
* operation hash is based on address in chain and ignored meta account id
*/
fun PendingMultisigOperation.Companion.createOperationHash(metaAccount: MetaAccount, chain: Chain, callHash: String): String {
return "${metaAccount.addressIn(chain)}:${chain.id}:$callHash"
.toByteArray()
.toHexString(withPrefix = true)
}
fun PendingMultisigOperationId.Companion.create(metaAccount: MetaAccount, chain: Chain, callHash: String): PendingMultisigOperationId {
return PendingMultisigOperationId(metaAccount.id, chain.id, callHash)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_account_api.data.multisig.repository
import io.novafoundation.nova.feature_account_api.domain.model.SavedMultisigOperationCall
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.flow.Flow
interface MultisigOperationLocalCallRepository {
suspend fun setMultisigCall(operation: SavedMultisigOperationCall)
fun callsFlow(): Flow<List<SavedMultisigOperationCall>>
suspend fun removeCallHashesExclude(metaId: Long, chainId: ChainId, excludedCallHashes: Set<CallHash>)
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_account_api.data.multisig.repository
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.common.utils.times
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface MultisigValidationsRepository {
suspend fun getMultisigDepositBase(chainId: ChainId): BalanceOf
suspend fun getMultisigDepositFactor(chainId: ChainId): BalanceOf
suspend fun hasPendingCallHash(chainId: ChainId, accountIdKey: AccountIdKey, callHash: CallHash): Boolean
}
suspend fun MultisigValidationsRepository.getMultisigDeposit(chainId: ChainId, threshold: Int): BalanceOf {
if (threshold == 1) return BalanceOf.ZERO
val base = getMultisigDepositBase(chainId)
val factor = getMultisigDepositFactor(chainId)
return base + factor * threshold
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_account_api.data.multisig.validation
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
sealed interface MultisigExtrinsicValidationFailure {
class NotEnoughSignatoryBalance(
val signatory: MetaAccount,
val asset: Chain.Asset,
val deposit: BigInteger?,
val fee: BigInteger?,
val balanceToAdd: BigInteger
) : MultisigExtrinsicValidationFailure
class OperationAlreadyExists(
val multisigAccount: MultisigMetaAccount
) : MultisigExtrinsicValidationFailure
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_account_api.data.multisig.validation
import io.novafoundation.nova.common.address.AccountIdKey
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.requireAccountIdIn
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
class MultisigExtrinsicValidationPayload(
val multisig: MultisigMetaAccount,
val signatory: MetaAccount,
val chain: Chain,
val signatoryFeePaymentMode: SignatoryFeePaymentMode,
// Call that is passed to as_multi. Might be both the actual call (in case multisig is a the root signer) or be wrapped by some other signer
val callInsideAsMulti: GenericCall.Instance,
)
sealed class SignatoryFeePaymentMode {
data object PaysSubmissionFee : SignatoryFeePaymentMode()
data object NothingToPay : SignatoryFeePaymentMode()
}
fun MultisigExtrinsicValidationPayload.signatoryAccountId(): AccountId {
return signatory.requireAccountIdIn(chain)
}
fun MultisigExtrinsicValidationPayload.multisigAccountId(): AccountIdKey {
return multisig.requireAccountIdKeyIn(chain)
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.data.multisig.validation
import io.novafoundation.nova.common.utils.bus.BaseRequestBus
import io.novafoundation.nova.common.utils.bus.RequestBus
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_account_api.data.multisig.validation.MultisigExtrinsicValidationRequestBus.Request
import io.novafoundation.nova.feature_account_api.data.multisig.validation.MultisigExtrinsicValidationRequestBus.ValidationResponse
class MultisigExtrinsicValidationRequestBus() : BaseRequestBus<Request, ValidationResponse>() {
class Request(val validationPayload: MultisigExtrinsicValidationPayload) : RequestBus.Request
class ValidationResponse(val validationResult: Result<ValidationStatus<MultisigExtrinsicValidationFailure>>) : RequestBus.Response
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_api.data.multisig.validation
import io.novafoundation.nova.common.validation.Validation
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
typealias MultisigExtrinsicValidationBuilder = ValidationSystemBuilder<MultisigExtrinsicValidationPayload, MultisigExtrinsicValidationFailure>
typealias MultisigExtrinsicValidation = Validation<MultisigExtrinsicValidationPayload, MultisigExtrinsicValidationFailure>
typealias MultisigExtrinsicValidationStatus = ValidationStatus<MultisigExtrinsicValidationFailure>
typealias MultisigExtrinsicValidationSystem = ValidationSystem<MultisigExtrinsicValidationPayload, MultisigExtrinsicValidationFailure>
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_account_api.data.proxy
import kotlinx.coroutines.flow.Flow
interface MetaAccountsUpdatesRegistry {
fun addMetaIds(ids: List<Long>)
fun observeUpdates(): Flow<Set<Long>>
fun getUpdates(): Set<Long>
fun remove(ids: List<Long>)
fun clear()
fun hasUpdates(): Boolean
fun observeUpdatesExist(): Flow<Boolean>
fun observeLastConsumedUpdatesMetaIds(): Flow<Set<Long>>
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_api.data.proxy.validation
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.ProxiedMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import java.math.BigInteger
class ProxiedExtrinsicValidationPayload(
val proxiedMetaAccount: ProxiedMetaAccount,
val proxyMetaAccount: MetaAccount,
val chainWithAsset: ChainWithAsset,
val proxiedCall: GenericCall.Instance
)
val ProxiedExtrinsicValidationPayload.proxyAccountId: AccountId
get() = proxyMetaAccount.requireAccountIdIn(chainWithAsset.chain)
sealed interface ProxiedExtrinsicValidationFailure {
class ProxyNotEnoughFee(
val proxy: MetaAccount,
val asset: Chain.Asset,
val fee: BigInteger,
val availableBalance: BigInteger
) : ProxiedExtrinsicValidationFailure
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.data.proxy.validation
import io.novafoundation.nova.common.utils.bus.BaseRequestBus
import io.novafoundation.nova.common.utils.bus.RequestBus
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus.Request
import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus.ValidationResponse
class ProxyExtrinsicValidationRequestBus() : BaseRequestBus<Request, ValidationResponse>() {
class Request(val validationPayload: ProxiedExtrinsicValidationPayload) : RequestBus.Request
class ValidationResponse(val validationResult: Result<ValidationStatus<ProxiedExtrinsicValidationFailure>>) : RequestBus.Response
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_account_api.data.repository
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.core.model.CryptoType
import io.novasama.substrate_sdk_android.scale.EncodableStruct
interface CreateSecretsRepository {
suspend fun createSecretsWithSeed(
seed: ByteArray,
cryptoType: CryptoType,
derivationPath: String?,
isEthereum: Boolean,
): EncodableStruct<ChainAccountSecrets>
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_account_api.data.repository
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.data.model.AccountAddressMap
import io.novafoundation.nova.feature_account_api.data.model.AccountIdKeyMap
import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
interface OnChainIdentityRepository {
@Deprecated("Use getIdentitiesFromIds instead to avoid extra from/to hex conversions")
suspend fun getIdentitiesFromIdsHex(chainId: ChainId, accountIdsHex: Collection<String>): AccountIdMap<OnChainIdentity?>
suspend fun getIdentitiesFromIds(accountIds: Collection<AccountId>, chainId: ChainId): AccountIdKeyMap<OnChainIdentity?>
suspend fun getIdentityFromId(chainId: ChainId, accountId: AccountId): OnChainIdentity?
suspend fun getMultiChainIdentities(accountIds: Collection<AccountIdKey>): AccountIdKeyMap<OnChainIdentity?>
@Deprecated("Use getIdentitiesFromIds instead to avoid extra from/to address conversions")
suspend fun getIdentitiesFromAddresses(chain: Chain, accountAddresses: List<String>): AccountAddressMap<OnChainIdentity?>
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_account_api.data.repository.addAccount
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus.Event
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
interface AddAccountRepository<T> {
suspend fun addAccount(payload: T): AddAccountResult
}
sealed interface AddAccountResult {
sealed interface HadEffect : AddAccountResult
interface SingleAccountChange {
val metaId: Long
}
class AccountAdded(override val metaId: Long, val type: LightMetaAccount.Type) : HadEffect, SingleAccountChange
class AccountChanged(override val metaId: Long, val type: LightMetaAccount.Type) : HadEffect, SingleAccountChange
class Batch(val updates: List<HadEffect>) : HadEffect
data object NoOp : AddAccountResult
}
fun AddAccountResult.toAccountBusEvent(): Event? {
return when (this) {
is AddAccountResult.HadEffect -> toAccountBusEvent()
is AddAccountResult.NoOp -> null
}
}
fun AddAccountResult.HadEffect.toAccountBusEvent(): Event {
return when (this) {
is AddAccountResult.AccountAdded -> Event.AccountAdded(metaId, type)
is AddAccountResult.AccountChanged -> Event.AccountStructureChanged(metaId, type)
is AddAccountResult.Batch -> Event.BatchUpdate(updates.map { it.toAccountBusEvent() })
}
}
suspend fun <T> AddAccountRepository<T>.addAccountWithSingleChange(payload: T): AddAccountResult.SingleAccountChange {
val result = addAccount(payload)
require(result is AddAccountResult.SingleAccountChange)
return result
}
fun List<AddAccountResult>.batchIfNeeded(): AddAccountResult {
val updatesThatHadEffect = filterIsInstance<AddAccountResult.HadEffect>()
return when (updatesThatHadEffect.size) {
0 -> AddAccountResult.NoOp
1 -> updatesThatHadEffect.single()
else -> AddAccountResult.Batch(updatesThatHadEffect)
}
}
fun AddAccountResult.visit(
onAdd: (AddAccountResult.AccountAdded) -> Unit
) {
when (this) {
is AddAccountResult.AccountAdded -> onAdd(this)
is AddAccountResult.AccountChanged -> Unit
is AddAccountResult.Batch -> updates.onEach { it.visit(onAdd) }
AddAccountResult.NoOp -> Unit
}
}
fun AddAccountResult.collectAddedIds(): List<Long> {
return buildList {
visit {
add(it.metaId)
}
}
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerEvmAccount
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
interface GenericLedgerAddAccountRepository : AddAccountRepository<GenericLedgerAddAccountRepository.Payload> {
sealed interface Payload {
class NewWallet(
val name: String,
val substrateAccount: LedgerSubstrateAccount,
val evmAccount: LedgerEvmAccount?,
) : Payload
class AddEvmAccount(
val metaId: Long,
val evmAccount: LedgerEvmAccount
) : Payload
}
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface LegacyLedgerAddAccountRepository : AddAccountRepository<LegacyLedgerAddAccountRepository.Payload> {
sealed interface Payload {
class MetaAccount(
val name: String,
val ledgerChainAccounts: Map<ChainId, LedgerSubstrateAccount>
) : Payload
class ChainAccount(
val metaId: Long,
val chainId: ChainId,
val ledgerChainAccount: LedgerSubstrateAccount
) : Payload
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_account_api.data.repository.addAccount.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.multisig.MultisigAddAccountRepository.Payload
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface MultisigAddAccountRepository : AddAccountRepository<Payload> {
class Payload(
val accounts: List<AccountPayload>
)
class AccountPayload(
val chain: Chain,
val multisigAccountId: AccountIdKey,
val otherSignatories: List<AccountIdKey>,
val threshold: Int,
val signatoryMetaId: Long,
val signatoryAccountId: AccountIdKey,
val identity: Identity?
)
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_account_api.data.repository.addAccount.proxied
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
interface ProxiedAddAccountRepository : AddAccountRepository<ProxiedAddAccountRepository.Payload> {
class Payload(
val chainId: ChainId,
val proxiedAccountId: AccountId,
val proxyType: ProxyType,
val proxyMetaId: Long,
val identity: Identity?
)
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.data.repository.addAccount.secrets
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository
import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
interface MnemonicAddAccountRepository : AddAccountRepository<MnemonicAddAccountRepository.Payload> {
class Payload(
val mnemonic: String,
val advancedEncryption: AdvancedEncryption,
val addAccountType: AddAccountType
)
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_account_api.data.secrets
import io.novafoundation.nova.common.data.secrets.v2.AccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.data.secrets.v2.getAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.mapChainAccountSecretsToKeypair
import io.novafoundation.nova.common.data.secrets.v2.mapMetaAccountSecretsToDerivationPath
import io.novafoundation.nova.common.data.secrets.v2.mapMetaAccountSecretsToKeypair
import io.novafoundation.nova.common.utils.fold
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.scale.EncodableStruct
suspend fun SecretStoreV2.getAccountSecrets(
metaAccount: MetaAccount,
chain: Chain
): AccountSecrets {
val accountId = metaAccount.accountIdIn(chain) ?: error("No account for chain $chain in meta account ${metaAccount.name}")
return getAccountSecrets(metaAccount.id, accountId)
}
fun AccountSecrets.keypair(chain: Chain): Keypair {
return fold(
left = { mapMetaAccountSecretsToKeypair(it, ethereum = chain.isEthereumBased) },
right = { mapChainAccountSecretsToKeypair(it) }
)
}
fun AccountSecrets.derivationPath(chain: Chain): String? {
return fold(
left = { mapMetaAccountSecretsToDerivationPath(it, ethereum = chain.isEthereumBased) },
right = { it[ChainAccountSecrets.DerivationPath] }
)
}
fun EncodableStruct<MetaAccountSecrets>.keypair(ethereum: Boolean): Keypair {
return mapMetaAccountSecretsToKeypair(this, ethereum)
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_account_api.data.signer
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType.DELAYED
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType.IMMEDIATE
/**
* Specifies whether the actual transaction call (e.g. transfer) will be executed immediately or delayed
*/
enum class CallExecutionType {
/**
* Actual call is executed immediately, together with the transaction itself
* This is the most common case
*/
IMMEDIATE,
/**
* Actual call's executed is delayed - transaction only executes preparation step
* Examples: multisig or delayed proxies operations
*/
DELAYED
}
fun CallExecutionType.isImmediate(): Boolean {
return this == IMMEDIATE
}
fun CallExecutionType.intersect(other: CallExecutionType): CallExecutionType {
return if (isImmediate() && other.isImmediate()) {
IMMEDIATE
} else {
DELAYED
}
}
@@ -0,0 +1,105 @@
package io.novafoundation.nova.feature_account_api.data.signer
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.extrinsic.signer.SignerPayloadRawWithChain
import io.novafoundation.nova.runtime.extrinsic.signer.withChain
import io.novafoundation.nova.runtime.extrinsic.signer.withoutChain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
interface NovaSigner : Signer {
/**
* Determines execution type of the actual call (e.g. transfer)
* Implementation node: signers that delegate signing to nested signers should intersect their execution type with the nested one
*/
suspend fun callExecutionType(): CallExecutionType
/**
* Meta account this signer was created for
* This is the same value that was passed to [SignerProvider.rootSignerFor] or [SignerProvider.nestedSignerFor]
*/
val metaAccount: MetaAccount
/**
* Returns full signing hierarchy for root and nested signers
*/
suspend fun getSigningHierarchy(): SubmissionHierarchy
/**
* Modify the extrinsic to enrich it with the signing data relevant for this type
* In all situations, at least nonce and signature will be required
* However some signers may even modify the call (e.g. Proxied signer will wrap the current call into proxy call)
*
* This should only be called after all other extrinsic information has been set, including all non-signer related extensions and calls
* So, this should be the final operation that modifies the extrinsic, followed just by [ExtrinsicBuilder.buildExtrinsic]
*
* Note for nested signers:
*
* Since signers delegation work in top-down approach(root signer is the executing account),
* but the wrapping should be done in the bottom-up way (the actual call is the inner-most one),
* nested signers should perform call wrapping themselves, and only after that perform nested [setSignerDataForSubmission] call.
*
* For example:
*
* With Secrets Wallet -> Proxy -> Multisig setup, the signing sequence will be MultisigSigner -> ProxiedSigner -> SecretsSigner.
* The final call should be proxy(as_multi(actual)) from Secrets origin.
* So, the wrapping should be done in the following sequence: actual -> wrap in as_multi -> wrap in proxy.
* So, the top-most signer (MultisigSigner) should first wrap the actual call into as_multi and only then delegate to ProxiedSigner.
*/
context(ExtrinsicBuilder)
suspend fun setSignerDataForSubmission(context: SigningContext)
/**
* Same as [setSignerDataForSubmission] but should use fake signature so signed extrinsic can be safely used for fee calculation
* This may also apply certain optimizations like hard-coding the nonce or other values to speedup the extrinsic construction
* and thus, fee calculation
*
* This should only be called after all other extrinsic information has been set, including all non-signer related extensions and calls
* So, this should be the final operation that modifies the extrinsic, followed just by [ExtrinsicBuilder.buildExtrinsic]
*
* You can find notes about nested signers in [setSignerDataForSubmission]
*/
context(ExtrinsicBuilder)
suspend fun setSignerDataForFee(context: SigningContext)
/**
* Return accountId of a signer that will actually sign this extrinsic
* For example, for Proxied account the actual signer is its Proxy
*/
suspend fun submissionSignerAccountId(chain: Chain): AccountId
/**
* Determines whether this particular instance of signer imposes additional limits to the number of calls
* it is possible to add to a single transaction.
* This is useful for signers that run in resource-constrained environment and thus cannot handle large transactions, e.g. Ledger
*/
suspend fun maxCallsPerTransaction(): Int?
// TODO this is a temp solution to workaround Polkadot Vault requiring chain id to sign a raw message
// This method should be removed once Vault behavior is improved
suspend fun signRawWithChain(payload: SignerPayloadRawWithChain): SignedRaw {
return signRaw(payload.withoutChain())
}
}
context(ExtrinsicBuilder)
suspend fun NovaSigner.setSignerData(context: SigningContext, mode: SigningMode) {
when (mode) {
SigningMode.FEE -> setSignerDataForFee(context)
SigningMode.SUBMISSION -> setSignerDataForSubmission(context)
}
}
suspend fun NovaSigner.signRaw(payloadRaw: SignerPayloadRaw, chainId: ChainId?): SignedRaw {
return if (chainId != null) {
signRawWithChain(payloadRaw.withChain(chainId))
} else {
signRaw(payloadRaw)
}
}
@@ -0,0 +1,48 @@
package io.novafoundation.nova.feature_account_api.data.signer
import io.novafoundation.nova.common.utils.MutableSharedState
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.extrinsic.signer.SignerPayloadRawWithChain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.getGenesisHashOrThrow
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.signingPayload
typealias SigningSharedState = MutableSharedState<SeparateFlowSignerState>
class SeparateFlowSignerState(val payload: SignerPayload, val metaAccount: MetaAccount)
sealed class SignerPayload {
class Extrinsic(val extrinsic: InheritedImplication, val accountId: AccountId) : SignerPayload()
class Raw(val raw: SignerPayloadRawWithChain) : SignerPayload()
}
fun SignerPayload.chainId(): ChainId {
return when (this) {
is SignerPayload.Extrinsic -> extrinsic.getGenesisHashOrThrow().toHexString()
is SignerPayload.Raw -> raw.chainId
}
}
fun SignerPayload.accountId(): AccountId {
return when (this) {
is SignerPayload.Extrinsic -> accountId
is SignerPayload.Raw -> raw.accountId
}
}
fun SignerPayload.signaturePayload(): ByteArray {
return when (this) {
is SignerPayload.Extrinsic -> extrinsic.signingPayload()
is SignerPayload.Raw -> raw.message
}
}
fun SeparateFlowSignerState.requireExtrinsic(): InheritedImplication {
require(payload is SignerPayload.Extrinsic)
return payload.extrinsic
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.data.signer
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
interface SignerProvider {
fun rootSignerFor(metaAccount: MetaAccount): NovaSigner
fun nestedSignerFor(metaAccount: MetaAccount): NovaSigner
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_account_api.data.signer
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.Nonce
interface SigningContext {
interface Factory {
fun default(chain: Chain): SigningContext
}
val chain: Chain
suspend fun getNonce(accountId: AccountIdKey): Nonce
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_account_api.data.signer
enum class SigningMode {
FEE, SUBMISSION
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_account_api.data.signer
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
/**
* A signing chain of accounts.
* Contains at least 1 item in path.
* Ordering of accounts is built in the following order:
* - path[0] always contains account for Leaf Signer
* - path[1] Nested account
* ...
* - path[n - 1] Nested account
* - path[n] is always Selected account
*
*/
class SubmissionHierarchy(
val path: List<Node>
) {
class Node(
val account: MetaAccount,
val callExecutionType: CallExecutionType
)
constructor(metaAccount: MetaAccount, callExecutionType: CallExecutionType) : this(listOf(Node(metaAccount, callExecutionType)))
operator fun plus(submissionHierarchy: SubmissionHierarchy): SubmissionHierarchy {
return SubmissionHierarchy(path + submissionHierarchy.path)
}
}
fun SubmissionHierarchy.isDelayed() = path.any { it.callExecutionType == CallExecutionType.DELAYED }
fun SubmissionHierarchy.selectedAccount() = path.last().account
@@ -0,0 +1,185 @@
package io.novafoundation.nova.feature_account_api.di
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationExecutor
import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory
import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.externalAccounts.ExternalAccountsSyncService
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.fee.FeePaymentProviderRegistry
import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector
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.data.multisig.validation.MultisigExtrinsicValidationRequestBus
import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry
import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus
import io.novafoundation.nova.feature_account_api.data.repository.CreateSecretsRepository
import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.secrets.MnemonicAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState
import io.novafoundation.nova.feature_account_api.di.deeplinks.AccountDeepLinks
import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults
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.account.identity.OnChainIdentity
import io.novafoundation.nova.feature_account_api.domain.cloudBackup.ApplyLocalSnapshotToCloudBackupUseCase
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.CreateGiftMetaAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.MetaAccountTypePresentationMapper
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.copyAddress.CopyAddressMixin
import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator
import io.novafoundation.nova.feature_account_api.presenatation.language.LanguageUseCase
import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.AddressInputMixinFactory
import io.novafoundation.nova.feature_account_api.presenatation.mixin.identity.IdentityMixin
import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserMixin
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressMixin
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectSingleWallet.SelectSingleWalletMixin
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletMixin
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
interface AccountFeatureApi {
val addressInputMixinFactory: AddressInputMixinFactory
val walletUiUseCase: WalletUiUseCase
val signerProvider: SignerProvider
val watchOnlyMissingKeysPresenter: WatchOnlyMissingKeysPresenter
val signSharedState: SigningSharedState
val onChainIdentityRepository: OnChainIdentityRepository
val metaAccountTypePresentationMapper: MetaAccountTypePresentationMapper
val legacyLedgerAddAccountRepository: LegacyLedgerAddAccountRepository
val genericLedgerAddAccountRepository: GenericLedgerAddAccountRepository
val evmTransactionService: EvmTransactionService
val identityMixinFactory: IdentityMixin.Factory
val languageUseCase: LanguageUseCase
val selectWalletMixinFactory: SelectWalletMixin.Factory
val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider
val selectAddressMixinFactory: SelectAddressMixin.Factory
val metaAccountChangesEventBus: MetaAccountChangesEventBus
val applyLocalSnapshotToCloudBackupUseCase: ApplyLocalSnapshotToCloudBackupUseCase
val feePaymentProviderRegistry: FeePaymentProviderRegistry
val customFeeCapabilityFacade: CustomFeeCapabilityFacade
val hydrationFeeInjector: HydrationFeeInjector
val addressActionsMixinFactory: AddressActionsMixin.Factory
val accountDeepLinks: AccountDeepLinks
val mnemonicAddAccountRepository: MnemonicAddAccountRepository
val multisigPendingOperationsService: MultisigPendingOperationsService
val signingContextFactory: SigningContext.Factory
val extrinsicSplitter: ExtrinsicSplitter
val externalAccountsSyncService: ExternalAccountsSyncService
val multisigValidationsRepository: MultisigValidationsRepository
val multisigExtrinsicValidationRequestBus: MultisigExtrinsicValidationRequestBus
val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper
val multisigOperationLocalCallRepository: MultisigOperationLocalCallRepository
val multisigFormatter: MultisigFormatter
val proxyFormatter: ProxyFormatter
val accountUIUseCase: AccountUIUseCase
val multisigDetailsRepository: MultisigDetailsRepository
val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry
val createSecretsRepository: CreateSecretsRepository
val createGiftMetaAccountUseCase: CreateGiftMetaAccountUseCase
val selectSingleWalletMixin: SelectSingleWalletMixin.Factory
@LocalIdentity
fun localIdentityProvider(): IdentityProvider
@OnChainIdentity
fun onChainIdentityProvider(): IdentityProvider
@LocalWithOnChainIdentity
fun localWithOnChainIdentityProvider(): IdentityProvider
fun metaAccountGroupingInteractor(): MetaAccountGroupingInteractor
fun accountInteractor(): AccountInteractor
fun provideAccountRepository(): AccountRepository
fun externalAccountActions(): ExternalActions.Presentation
fun accountUpdateScope(): AccountUpdateScope
fun addressDisplayUseCase(): AddressDisplayUseCase
fun accountUseCase(): SelectedAccountUseCase
fun extrinsicService(): ExtrinsicService
fun extrinsicServiceFactory(): ExtrinsicService.Factory
fun importTypeChooserMixin(): ImportTypeChooserMixin.Presentation
fun twoFactorVerificationExecutor(): TwoFactorVerificationExecutor
fun biometricServiceFactory(): BiometricServiceFactory
fun encryptionDefaults(): EncryptionDefaults
fun proxyExtrinsicValidationRequestBus(): ProxyExtrinsicValidationRequestBus
fun cloudBackupFacade(): LocalAccountsCloudBackupFacade
fun syncWalletsBackupPasswordCommunicator(): SyncWalletsBackupPasswordCommunicator
fun copyAddressMixin(): CopyAddressMixin
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_account_api.di.deeplinks
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
class AccountDeepLinks(val deepLinkHandlers: List<DeepLinkHandler>)
@@ -0,0 +1,51 @@
package io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption
import io.novafoundation.nova.common.utils.input.Input
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults
class AdvancedEncryptionInput(
val substrateCryptoType: Input<CryptoType>,
val substrateDerivationPath: Input<String>,
val ethereumCryptoType: Input<CryptoType>,
val ethereumDerivationPath: Input<String>
)
data class AdvancedEncryption(
val substrateCryptoType: CryptoType?,
val ethereumCryptoType: CryptoType?,
val derivationPaths: DerivationPaths
) {
companion object;
data class DerivationPaths(
val substrate: String?,
val ethereum: String?
) {
companion object {
fun empty() = DerivationPaths(null, null)
}
}
}
fun EncryptionDefaults.recommended() = AdvancedEncryption(
substrateCryptoType = substrateCryptoType,
ethereumCryptoType = ethereumCryptoType,
derivationPaths = AdvancedEncryption.DerivationPaths(
substrate = substrateDerivationPath,
ethereum = ethereumDerivationPath
)
)
fun AdvancedEncryption.Companion.substrate(
cryptoType: CryptoType,
substrateDerivationPaths: String?
) = AdvancedEncryption(
substrateCryptoType = cryptoType,
ethereumCryptoType = null,
derivationPaths = AdvancedEncryption.DerivationPaths(
substrate = substrateDerivationPaths,
ethereum = null
)
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_account_api.domain.account.common
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class ChainWithAccountId(
val chain: Chain,
val accountId: ByteArray
)
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_api.domain.account.common
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class EncryptionDefaults(
val substrateCryptoType: CryptoType,
val ethereumCryptoType: CryptoType,
val substrateDerivationPath: String,
val ethereumDerivationPath: String
)
class ChainEncryptionDefaults(
val cryptoType: CryptoType,
val derivationPath: String
)
fun EncryptionDefaults.forChain(chain: Chain): ChainEncryptionDefaults {
return if (chain.isEthereumBased) {
ChainEncryptionDefaults(
cryptoType = ethereumCryptoType,
derivationPath = ethereumDerivationPath
)
} else {
ChainEncryptionDefaults(
cryptoType = substrateCryptoType,
derivationPath = substrateDerivationPath
)
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_account_api.domain.account.identity
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
data class Identity(val name: String)
fun Identity(onChainIdentity: OnChainIdentity): Identity? {
return onChainIdentity.display?.let { Identity(it) }
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.domain.account.identity
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.presentation.ellipsizeAddress
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
suspend fun IdentityProvider.getNameOrAddress(accountId: AccountIdKey, chain: Chain): String {
return identityFor(accountId.value, chain.id)?.name ?: chain.addressOf(accountId).ellipsizeAddress()
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_account_api.domain.account.identity
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
interface IdentityProvider {
companion object;
/**
* Returns, if present, an identity for the given [accountId] inside specified [chainId]
*/
suspend fun identityFor(accountId: AccountId, chainId: ChainId): Identity?
/**
* Bulk version of [identityFor]. Default implementation is unoptimized and just performs N single requests to [identityFor].
*/
suspend fun identitiesFor(accountIds: Collection<AccountId>, chainId: ChainId): Map<AccountIdKey, Identity?> {
return accountIds.associateBy(
keySelector = ::AccountIdKey,
valueTransform = { identityFor(it, chainId) }
)
}
}
fun IdentityProvider.Companion.oneOf(vararg delegates: IdentityProvider): IdentityProvider {
return OneOfIdentityProvider(delegates.toList())
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_account_api.domain.account.identity
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.extensions.tryFindNonNull
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class OneOfIdentityProvider(
private val delegates: List<IdentityProvider>
) : IdentityProvider {
override suspend fun identityFor(accountId: AccountId, chainId: ChainId): Identity? = withContext(Dispatchers.IO) {
delegates.tryFindNonNull {
it.identityFor(accountId, chainId)
}
}
override suspend fun identitiesFor(accountIds: Collection<AccountId>, chainId: ChainId): Map<AccountIdKey, Identity?> = withContext(Dispatchers.IO) {
delegates.tryFindNonNull {
it.identitiesFor(accountIds, chainId)
}.orEmpty()
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_account_api.domain.account.identity
import javax.inject.Qualifier
@Qualifier
annotation class OnChainIdentity
@Qualifier
annotation class LocalIdentity
@Qualifier
annotation class LocalWithOnChainIdentity
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_api.domain.account.system
import io.novafoundation.nova.common.address.AccountIdKey
import io.novasama.substrate_sdk_android.runtime.AccountId
class AccountSystemAccountMatcher(private val accountIdKey: AccountIdKey) : SystemAccountMatcher {
override fun isSystemAccount(accountId: AccountId): Boolean {
return accountIdKey.value.contentEquals(accountId)
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.domain.account.system
import io.novasama.substrate_sdk_android.runtime.AccountId
class CompoundSystemAccountMatcher(
private val delegates: List<SystemAccountMatcher>
) : SystemAccountMatcher {
constructor(vararg delegates: SystemAccountMatcher) : this(delegates.toList())
override fun isSystemAccount(accountId: AccountId): Boolean {
return delegates.any { it.isSystemAccount(accountId) }
}
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_account_api.domain.account.system
import io.novafoundation.nova.common.utils.startsWith
import io.novasama.substrate_sdk_android.runtime.AccountId
class PrefixSystemAccountMatcher(private val prefix: ByteArray) : SystemAccountMatcher {
constructor(utf8Prefix: String) : this(utf8Prefix.encodeToByteArray())
override fun isSystemAccount(accountId: AccountId): Boolean {
return accountId.startsWith(prefix)
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_account_api.domain.account.system
import io.novasama.substrate_sdk_android.runtime.AccountId
interface SystemAccountMatcher {
companion object
fun isSystemAccount(accountId: AccountId): Boolean
}
fun SystemAccountMatcher.Companion.default(): SystemAccountMatcher {
return CompoundSystemAccountMatcher(
// Pallet-specific technical accounts, e.g. crowdloan-fund, nomination pool,
PrefixSystemAccountMatcher("modl"),
// Parachain sovereign accounts on relaychain
PrefixSystemAccountMatcher("para"),
// Relaychain sovereign account on parachains
PrefixSystemAccountMatcher("Parent"),
// Sibling parachain soveregin accounts
PrefixSystemAccountMatcher("sibl")
)
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_account_api.domain.cloudBackup
interface ApplyLocalSnapshotToCloudBackupUseCase {
suspend fun applyLocalSnapshotToCloudBackupIfSyncEnabled(): Result<Unit>
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_account_api.domain.filter.selectAddress
import io.novafoundation.nova.common.utils.Filter
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.isControllableWallet
sealed interface SelectAccountFilter : Filter<MetaAccount> {
class Everything : SelectAccountFilter {
override fun shouldInclude(model: MetaAccount): Boolean {
return true
}
}
class ControllableWallets() : SelectAccountFilter {
override fun shouldInclude(model: MetaAccount): Boolean {
return model.type.isControllableWallet()
}
}
class ExcludeMetaAccounts(val metaIds: List<Long>) : SelectAccountFilter {
override fun shouldInclude(model: MetaAccount): Boolean {
return !metaIds.contains(model.id)
}
}
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core.model.Language
import io.novafoundation.nova.core.model.Node
import io.novafoundation.nova.feature_account_api.domain.model.Account
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.PreferredCryptoType
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.encrypt.mnemonic.Mnemonic
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
interface AccountInteractor {
suspend fun getActiveMetaAccounts(): List<MetaAccount>
suspend fun generateMnemonic(): Mnemonic
fun getCryptoTypes(): List<CryptoType>
suspend fun getPreferredCryptoType(chainId: ChainId? = null): PreferredCryptoType
suspend fun isCodeSet(): Boolean
suspend fun savePin(code: String)
suspend fun isPinCorrect(code: String): Boolean
suspend fun getMetaAccount(metaId: Long): MetaAccount
suspend fun selectMetaAccount(metaId: Long)
suspend fun selectedMetaAccount(): MetaAccount
suspend fun deleteAccount(metaId: Long): Boolean
suspend fun updateMetaAccountPositions(idsInNewOrder: List<Long>)
fun chainFlow(chainId: ChainId): Flow<Chain>
fun nodesFlow(): Flow<List<Node>>
suspend fun getNode(nodeId: Int): Node
fun getLanguages(): List<Language>
suspend fun getSelectedLanguage(): Language
suspend fun changeSelectedLanguage(language: Language)
suspend fun addNode(nodeName: String, nodeHost: String): Result<Unit>
suspend fun updateNode(nodeId: Int, newName: String, newHost: String): Result<Unit>
suspend fun getAccountsByNetworkTypeWithSelectedNode(networkType: Node.NetworkType): Pair<List<Account>, Node>
suspend fun selectNodeAndAccount(nodeId: Int, accountAddress: String)
suspend fun selectNode(nodeId: Int)
suspend fun deleteNode(nodeId: Int)
suspend fun getChainAddress(metaId: Long, chainId: ChainId): String?
suspend fun removeDeactivatedMetaAccounts()
suspend fun switchToNotDeactivatedAccountIfNeeded()
suspend fun hasSecretsAccounts(): Boolean
suspend fun hasCustomChainAccounts(metaId: Long): Boolean
suspend fun deleteProxiedMetaAccountsByChain(chainId: String)
suspend fun findMetaAccount(chain: Chain, value: AccountId): MetaAccount?
}
@@ -0,0 +1,148 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core.model.Language
import io.novafoundation.nova.core.model.Node
import io.novafoundation.nova.feature_account_api.domain.model.Account
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.MetaAccountAssetBalance
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountOrdering
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.encrypt.mnemonic.Mnemonic
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
class AccountAlreadyExistsException : Exception()
interface AccountRepository {
fun getEncryptionTypes(): List<CryptoType>
suspend fun getNode(nodeId: Int): Node
suspend fun getSelectedNodeOrDefault(): Node
suspend fun selectNode(node: Node)
suspend fun getDefaultNode(networkType: Node.NetworkType): Node
suspend fun selectAccount(account: Account, newNode: Node? = null)
suspend fun getSelectedMetaAccount(): MetaAccount
suspend fun getMetaAccount(metaId: Long): MetaAccount
fun metaAccountFlow(metaId: Long): Flow<MetaAccount>
fun selectedMetaAccountFlow(): Flow<MetaAccount>
suspend fun findMetaAccount(accountId: ByteArray, chainId: ChainId): MetaAccount?
suspend fun accountNameFor(accountId: AccountId, chainId: ChainId): String?
suspend fun hasActiveMetaAccounts(): Boolean
fun allMetaAccountsFlow(): Flow<List<MetaAccount>>
fun activeMetaAccountsFlow(): Flow<List<MetaAccount>>
fun metaAccountsByTypeFlow(type: LightMetaAccount.Type): Flow<List<MetaAccount>>
fun metaAccountBalancesFlow(): Flow<List<MetaAccountAssetBalance>>
fun metaAccountBalancesFlow(metaId: Long): Flow<List<MetaAccountAssetBalance>>
suspend fun selectMetaAccount(metaId: Long)
suspend fun updateMetaAccountName(metaId: Long, newName: String)
suspend fun isAccountSelected(): Boolean
suspend fun deleteAccount(metaId: Long)
suspend fun getAccounts(): List<Account>
suspend fun getAccount(address: String): Account
suspend fun getAccountOrNull(address: String): Account?
suspend fun getMyAccounts(query: String, chainId: String): Set<Account>
suspend fun isCodeSet(): Boolean
suspend fun savePinCode(code: String)
suspend fun getPinCode(): String?
suspend fun generateMnemonic(): Mnemonic
fun isBiometricEnabledFlow(): Flow<Boolean>
fun isBiometricEnabled(): Boolean
fun setBiometricOn()
fun setBiometricOff()
fun nodesFlow(): Flow<List<Node>>
suspend fun updateAccountsOrdering(accountOrdering: List<MetaAccountOrdering>)
fun getLanguages(): List<Language>
suspend fun selectedLanguage(): Language
suspend fun changeLanguage(language: Language)
suspend fun addNode(nodeName: String, nodeHost: String, networkType: Node.NetworkType)
suspend fun updateNode(nodeId: Int, newName: String, newHost: String, networkType: Node.NetworkType)
suspend fun checkNodeExists(nodeHost: String): Boolean
/**
* @throws NovaException
* @throws NovaException
*/
suspend fun getNetworkName(nodeHost: String): String
suspend fun getAccountsByNetworkType(networkType: Node.NetworkType): List<Account>
suspend fun deleteNode(nodeId: Int)
suspend fun createQrAccountContent(chain: Chain, account: MetaAccount): String
suspend fun generateRestoreJson(
metaAccount: MetaAccount,
chain: Chain,
password: String
): String
suspend fun isAccountExists(accountId: AccountId, chainId: ChainId): Boolean
suspend fun removeDeactivatedMetaAccounts()
suspend fun getActiveMetaAccounts(): List<MetaAccount>
suspend fun getAllMetaAccounts(): List<MetaAccount>
suspend fun getActiveMetaAccountsQuantity(): Int
fun hasMetaAccountsCountOfTypeFlow(type: LightMetaAccount.Type): Flow<Boolean>
suspend fun hasMetaAccountsByType(type: LightMetaAccount.Type): Boolean
suspend fun hasMetaAccountsByType(metaIds: Set<Long>, type: LightMetaAccount.Type): Boolean
suspend fun generateRestoreJson(metaAccount: MetaAccount, password: String): String
suspend fun hasSecretsAccounts(): Boolean
suspend fun deleteProxiedMetaAccountsByChain(chainId: String)
suspend fun getMetaAccountsByIds(metaIds: List<Long>): List<MetaAccount>
suspend fun getAvailableMetaIdsFromSet(metaIds: Set<Long>): Set<Long>
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.AccountId
suspend fun AccountRepository.findMetaAccountOrThrow(accountId: AccountId, chainId: ChainId) = findMetaAccount(accountId, chainId)
?: error("No meta account found for accountId: ${accountId.toHexString()}")
suspend fun AccountRepository.requireIdOfSelectedMetaAccountIn(chain: Chain): AccountId {
val metaAccount = getSelectedMetaAccount()
return metaAccount.requireAccountIdIn(chain)
}
suspend fun AccountRepository.requireIdKeyOfSelectedMetaAccountIn(chain: Chain): AccountIdKey {
return requireIdOfSelectedMetaAccountIn(chain).intoKey()
}
suspend fun AccountRepository.getIdOfSelectedMetaAccountIn(chain: Chain): AccountId? {
val metaAccount = getSelectedMetaAccount()
return metaAccount.accountIdIn(chain)
}
suspend fun AccountRepository.requireMetaAccountFor(transactionOrigin: TransactionOrigin, chainId: ChainId): MetaAccount {
return when (transactionOrigin) {
TransactionOrigin.SelectedWallet -> getSelectedMetaAccount()
is TransactionOrigin.WalletWithAccount -> findMetaAccountOrThrow(transactionOrigin.accountId, chainId)
is TransactionOrigin.Wallet -> transactionOrigin.metaAccount
is TransactionOrigin.WalletWithId -> getMetaAccount(transactionOrigin.metaId)
}
}
@@ -0,0 +1,108 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import android.graphics.drawable.Drawable
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.address.AddressIconGenerator.Companion.BACKGROUND_TRANSPARENT
import io.novafoundation.nova.common.address.AddressIconGenerator.Companion.SIZE_MEDIUM
import io.novafoundation.nova.common.address.AddressModel
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.accountIdKeyIn
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAddressModel
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletModel
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
sealed interface AccountModel {
fun address(): String
fun drawable(): Drawable?
fun nameOrAddress(): String
class Wallet(
metaId: Long,
name: String,
icon: Drawable?,
private val address: String
) : WalletModel(metaId, name, icon), AccountModel {
constructor(walletModel: WalletModel, address: String) : this(walletModel.metaId, walletModel.name, walletModel.icon, address)
override fun address() = address
override fun drawable() = icon
override fun nameOrAddress() = name
}
class Address(
address: String,
image: Drawable,
name: String? = null
) : AddressModel(address, image, name), AccountModel {
constructor(addressModel: AddressModel) : this(addressModel.address, addressModel.image, addressModel.name)
override fun address() = address
override fun drawable() = image
override fun nameOrAddress() = nameOrAddress
}
}
interface AccountUIUseCase {
suspend fun getAccountModel(accountId: AccountIdKey, chain: Chain): AccountModel
suspend fun getAccountModels(accountIds: Set<AccountIdKey>, chain: Chain): Map<AccountIdKey, AccountModel>
}
class RealAccountUIUseCase(
private val accountRepository: AccountRepository,
private val walletUiUseCase: WalletUiUseCase,
private val addressIconGenerator: AddressIconGenerator,
private val identityProvider: IdentityProvider
) : AccountUIUseCase {
override suspend fun getAccountModel(accountId: AccountIdKey, chain: Chain): AccountModel {
return getAccountModelInternal(
accountId,
chain,
accountRepository.findMetaAccount(accountId.value, chain.id),
identityProvider.identityFor(accountId.value, chain.id)
)
}
override suspend fun getAccountModels(accountIds: Set<AccountIdKey>, chain: Chain): Map<AccountIdKey, AccountModel> {
val identities = identityProvider.identitiesFor(accountIds.map { it.value }, chain.id)
val metaAccounts = accountRepository.getActiveMetaAccounts().associateBy { it.accountIdKeyIn(chain) }
return accountIds.associateWith { accountId ->
val metaAccount = metaAccounts[accountId]
val identity = identities[accountId]
getAccountModelInternal(accountId, chain, metaAccount, identity)
}
}
private suspend fun getAccountModelInternal(accountId: AccountIdKey, chain: Chain, metaAccount: MetaAccount?, identity: Identity?): AccountModel {
return when (metaAccount) {
null -> {
val addressModel = addressIconGenerator.createAddressModel(
chain,
chain.addressOf(accountId),
SIZE_MEDIUM,
accountName = identity?.name,
background = BACKGROUND_TRANSPARENT
)
AccountModel.Address(addressModel)
}
else -> {
val walletModel = walletUiUseCase.walletUiFor(metaAccount)
AccountModel.Wallet(walletModel, chain.addressOf(accountId))
}
}
}
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.scale.EncodableStruct
interface CreateGiftMetaAccountUseCase {
fun createTemporaryGiftMetaAccount(chain: Chain, chainSecrets: EncodableStruct<ChainAccountSecrets>): MetaAccount
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import io.novafoundation.nova.common.list.GroupedList
import io.novafoundation.nova.common.utils.Filter
import io.novafoundation.nova.feature_account_api.domain.model.AccountDelegation
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.MetaAccountListingItem
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.flow.Flow
interface MetaAccountGroupingInteractor {
fun metaAccountsWithTotalBalanceFlow(): Flow<GroupedList<LightMetaAccount.Type, MetaAccountListingItem>>
fun metaAccountWithTotalBalanceFlow(metaId: Long): Flow<MetaAccountListingItem>
fun getMetaAccountsWithFilter(metaAccountFilter: Filter<MetaAccount>): Flow<GroupedList<LightMetaAccount.Type, MetaAccount>>
fun updatedDelegates(): Flow<GroupedList<LightMetaAccount.Status, AccountDelegation>>
suspend fun hasAvailableMetaAccountsForChain(
chainId: ChainId,
metaAccountFilter: Filter<MetaAccount>
): Boolean
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_account_api.domain.interfaces
import android.graphics.drawable.Drawable
import io.novafoundation.nova.common.address.AddressModel
import io.novafoundation.nova.common.view.TintedIcon
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
class SelectedWalletModel(
val typeIcon: TintedIcon?,
val walletIcon: Drawable,
val name: String,
val hasUpdates: Boolean,
)
interface SelectedAccountUseCase {
fun selectedMetaAccountFlow(): Flow<MetaAccount>
fun selectedAddressModelFlow(chain: suspend () -> Chain): Flow<AddressModel>
fun selectedWalletModelFlow(): Flow<SelectedWalletModel>
suspend fun getSelectedMetaAccount(): MetaAccount
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core.model.Network
import io.novasama.substrate_sdk_android.extensions.fromHex
data class Account(
val address: String,
val name: String?,
val accountIdHex: String,
val cryptoType: CryptoType, // TODO make optional
val position: Int,
val network: Network, // TODO remove when account management will be rewritten,
) {
val accountId = accountIdHex.fromHex()
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
sealed class AddAccountType {
class MetaAccount(val name: String) : AddAccountType()
class ChainAccount(val chainId: ChainId, val metaId: Long) : AddAccountType()
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_account_api.domain.model
enum class AuthType {
PINCODE,
BIOMETRY
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.domain.model
fun metaAccountTypeComparator() = compareBy<LightMetaAccount.Type> {
when (it) {
LightMetaAccount.Type.SECRETS -> 0
LightMetaAccount.Type.POLKADOT_VAULT -> 1
LightMetaAccount.Type.PARITY_SIGNER -> 2
LightMetaAccount.Type.LEDGER -> 3
LightMetaAccount.Type.LEDGER_LEGACY -> 4
LightMetaAccount.Type.PROXIED -> 5
LightMetaAccount.Type.MULTISIG -> 6
LightMetaAccount.Type.WATCH_ONLY -> 7
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.core.model.CryptoType
class ImportJsonMetaData(
val name: String?,
val chainId: String?,
val encryptionType: CryptoType
)
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_account_api.domain.model
enum class LedgerVariant {
LEGACY, GENERIC
}
@@ -0,0 +1,298 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.mappers.mapCryptoTypeToEncryption
import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType
import io.novafoundation.nova.common.utils.DEFAULT_PREFIX
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.toEthereumAddress
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.encrypt.MultiChainEncryption
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
import io.novasama.substrate_sdk_android.extensions.toAccountId
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.ss58.SS58Encoder
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAddress
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
class MetaIdWithType(
val metaId: Long,
val type: LightMetaAccount.Type
)
class MetaAccountOrdering(
val id: Long,
val position: Int,
)
interface LightMetaAccount {
val id: Long
/**
* In contrast to [id] which should only be unique **locally**, [globallyUniqueId] should be unique **globally**,
* meaning it should be unique across all application instances. This is useful to compare meta accounts from different application instances
*/
val globallyUniqueId: String
val substratePublicKey: ByteArray?
val substrateCryptoType: CryptoType?
val substrateAccountId: ByteArray?
val ethereumAddress: ByteArray?
val ethereumPublicKey: ByteArray?
val isSelected: Boolean
val name: String
val type: Type
val status: Status
val parentMetaId: Long?
enum class Type {
SECRETS,
WATCH_ONLY,
PARITY_SIGNER,
LEDGER_LEGACY,
LEDGER,
POLKADOT_VAULT,
PROXIED,
MULTISIG
}
enum class Status {
ACTIVE, DEACTIVATED
}
}
fun LightMetaAccount(
id: Long,
globallyUniqueId: String,
substratePublicKey: ByteArray?,
substrateCryptoType: CryptoType?,
substrateAccountId: ByteArray?,
ethereumAddress: ByteArray?,
ethereumPublicKey: ByteArray?,
isSelected: Boolean,
name: String,
type: LightMetaAccount.Type,
status: LightMetaAccount.Status,
parentMetaId: Long?,
) = object : LightMetaAccount {
override val id: Long = id
override val globallyUniqueId: String = globallyUniqueId
override val substratePublicKey: ByteArray? = substratePublicKey
override val substrateCryptoType: CryptoType? = substrateCryptoType
override val substrateAccountId: ByteArray? = substrateAccountId
override val ethereumAddress: ByteArray? = ethereumAddress
override val ethereumPublicKey: ByteArray? = ethereumPublicKey
override val isSelected: Boolean = isSelected
override val name: String = name
override val type: LightMetaAccount.Type = type
override val status: LightMetaAccount.Status = status
override val parentMetaId: Long? = parentMetaId
}
interface MetaAccount : LightMetaAccount {
// TODO this should not be exposed as its a implementation detail
// We should rather use something like
// fun iterateAccounts(): Iterable<(AccountId, ChainId?, MultiChainEncryption?)>
val chainAccounts: Map<ChainId, ChainAccount>
class ChainAccount(
val metaId: Long,
val chainId: ChainId,
val publicKey: ByteArray?,
val accountId: ByteArray,
// TODO this should be MultiChainEncryption
val cryptoType: CryptoType?,
)
suspend fun supportsAddingChainAccount(chain: Chain): Boolean
fun hasAccountIn(chain: Chain): Boolean
fun accountIdIn(chain: Chain): AccountId?
fun publicKeyIn(chain: Chain): ByteArray?
}
interface SecretsMetaAccount : MetaAccount {
fun multiChainEncryptionIn(chain: Chain): MultiChainEncryption?
}
interface ProxiedMetaAccount : MetaAccount {
val proxy: ProxyAccount
}
interface MultisigMetaAccount : MetaAccount {
val signatoryMetaId: Long
val signatoryAccountId: AccountIdKey
/**
* A **sorted** list of other signatories signatories of the account
*/
val otherSignatories: List<AccountIdKey>
val threshold: Int
val availability: MultisigAvailability
}
sealed class MultisigAvailability {
class Universal(val addressScheme: AddressScheme) : MultisigAvailability()
class SingleChain(val chainId: ChainId) : MultisigAvailability()
}
fun MetaAccount.isUniversal(): Boolean {
return substrateAccountId != null || ethereumAddress != null
}
fun MultisigAvailability.singleChainId(): ChainId? {
return when (this) {
is MultisigAvailability.SingleChain -> chainId
is MultisigAvailability.Universal -> null
}
}
fun MultisigMetaAccount.isThreshold1(): Boolean {
return threshold == 1
}
fun MetaAccount.requireMultisigAccount() = this as MultisigMetaAccount
fun MetaAccount.hasChainAccountIn(chainId: ChainId) = chainId in chainAccounts
fun MetaAccount.addressIn(chain: Chain): String? {
return accountIdIn(chain)?.let(chain::addressOf)
}
fun MetaAccount.accountIdKeyIn(chain: Chain): AccountIdKey? {
return accountIdIn(chain)?.let(::AccountIdKey)
}
fun MetaAccount.mainEthereumAddress() = ethereumAddress?.toEthereumAddress()
fun MetaAccount.requireAddressIn(chain: Chain): String = addressIn(chain) ?: throw NoSuchElementException("No chain account found for ${chain.name} in $name")
val MetaAccount.defaultSubstrateAddress: String?
get() = substrateAccountId?.toDefaultSubstrateAddress()
fun ByteArray.toDefaultSubstrateAddress(): String {
return toAddress(SS58Encoder.DEFAULT_PREFIX)
}
fun MetaAccount.substrateMultiChainEncryption(): MultiChainEncryption? {
return substrateCryptoType?.let(MultiChainEncryption.Companion::substrateFrom)
}
fun MetaAccount.requireAccountIdIn(chain: Chain): ByteArray {
return requireNotNull(accountIdIn(chain))
}
fun MetaAccount.requireAccountIdKeyIn(chain: Chain): AccountIdKey {
return requireAccountIdIn(chain).intoKey()
}
fun MetaAccount.multiChainEncryptionIn(chain: Chain): MultiChainEncryption? {
return (this as? SecretsMetaAccount)?.multiChainEncryptionIn(chain)
}
fun MetaAccount.cryptoTypeIn(chain: Chain): CryptoType? {
return multiChainEncryptionIn(chain)?.toCryptoType()
}
private fun MultiChainEncryption.toCryptoType(): CryptoType {
return when (this) {
is MultiChainEncryption.Substrate -> mapEncryptionToCryptoType(encryptionType)
MultiChainEncryption.Ethereum -> CryptoType.ECDSA
}
}
fun MultiChainEncryption.Companion.substrateFrom(cryptoType: CryptoType): MultiChainEncryption.Substrate {
return MultiChainEncryption.Substrate(mapCryptoTypeToEncryption(cryptoType))
}
fun MetaAccount.ethereumAccountId() = ethereumPublicKey?.asEthereumPublicKey()?.toAccountId()?.value
fun MetaAccount.chainAccountFor(chainId: ChainId) = chainAccounts.getValue(chainId)
fun LightMetaAccount.Type.asPolkadotVaultVariantOrNull(): PolkadotVaultVariant? {
return when (this) {
LightMetaAccount.Type.PARITY_SIGNER -> PolkadotVaultVariant.PARITY_SIGNER
LightMetaAccount.Type.POLKADOT_VAULT -> PolkadotVaultVariant.POLKADOT_VAULT
else -> null
}
}
fun LightMetaAccount.Type.asPolkadotVaultVariantOrThrow(): PolkadotVaultVariant {
return requireNotNull(asPolkadotVaultVariantOrNull()) {
"Not a Polkadot Vault compatible account type"
}
}
fun LightMetaAccount.Type.requestedAccountPaysFees(): Boolean {
return when (this) {
LightMetaAccount.Type.SECRETS,
LightMetaAccount.Type.WATCH_ONLY,
LightMetaAccount.Type.PARITY_SIGNER,
LightMetaAccount.Type.LEDGER_LEGACY,
LightMetaAccount.Type.LEDGER,
LightMetaAccount.Type.POLKADOT_VAULT -> true
LightMetaAccount.Type.PROXIED,
LightMetaAccount.Type.MULTISIG -> false
}
}
fun LightMetaAccount.Type.isControllableWallet(): Boolean {
return when (this) {
LightMetaAccount.Type.SECRETS,
LightMetaAccount.Type.PARITY_SIGNER,
LightMetaAccount.Type.LEDGER_LEGACY,
LightMetaAccount.Type.LEDGER,
LightMetaAccount.Type.POLKADOT_VAULT -> true
LightMetaAccount.Type.WATCH_ONLY,
LightMetaAccount.Type.PROXIED,
LightMetaAccount.Type.MULTISIG -> false
}
}
@OptIn(ExperimentalContracts::class)
fun LightMetaAccount.isProxied(): Boolean {
contract {
returns(true) implies (this@isProxied is ProxiedMetaAccount)
}
return this is ProxiedMetaAccount
}
@OptIn(ExperimentalContracts::class)
fun LightMetaAccount.isMultisig(): Boolean {
contract {
returns(true) implies (this@isMultisig is MultisigMetaAccount)
}
return this is MultisigMetaAccount
}
fun LightMetaAccount.asProxied(): ProxiedMetaAccount = this as ProxiedMetaAccount
fun LightMetaAccount.asMultisig(): MultisigMetaAccount = this as MultisigMetaAccount
fun MultisigMetaAccount.signatoriesCount() = 1 + otherSignatories.size
fun MultisigMetaAccount.allSignatories() = buildSet {
add(signatoryAccountId)
addAll(otherSignatories.toSet())
}
@@ -0,0 +1,52 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.common.utils.Precision
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigDecimal
import java.math.BigInteger
class MetaAccountAssetBalance(
val metaId: Long,
val freeInPlanks: BigInteger,
val reservedInPlanks: BigInteger,
val offChainBalance: BigInteger?,
val precision: Precision,
val rate: BigDecimal?
)
sealed interface MetaAccountListingItem {
val metaAccount: MetaAccount
val hasUpdates: Boolean
val totalBalance: BigDecimal
val currency: Currency
class Proxied(
val proxyMetaAccount: MetaAccount,
val proxyChain: Chain,
override val totalBalance: BigDecimal,
override val currency: Currency,
override val metaAccount: ProxiedMetaAccount,
override val hasUpdates: Boolean
) : MetaAccountListingItem
class Multisig(
val signatory: MetaAccount,
val singleChain: Chain?, // null in case multisig is universal
override val totalBalance: BigDecimal,
override val currency: Currency,
override val metaAccount: MultisigMetaAccount,
override val hasUpdates: Boolean
) : MetaAccountListingItem
class TotalBalance(
override val totalBalance: BigDecimal,
override val currency: Currency,
override val metaAccount: MetaAccount,
override val hasUpdates: Boolean
) : MetaAccountListingItem
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_account_api.domain.model
enum class PolkadotVaultVariant {
POLKADOT_VAULT, PARITY_SIGNER
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.core.model.CryptoType
data class PreferredCryptoType(
val cryptoType: CryptoType,
val frozen: Boolean
)
@@ -0,0 +1,33 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
sealed interface AccountDelegation {
val delegator: MetaAccount
class Proxy(
val proxied: ProxiedMetaAccount,
val proxy: MetaAccount,
val chain: Chain
) : AccountDelegation {
override val delegator = proxied
}
class Multisig(
val metaAccount: MultisigMetaAccount,
val signatory: MetaAccount,
val singleChain: Chain?, // null in case multisig is universal
) : AccountDelegation {
override val delegator = metaAccount
}
}
fun AccountDelegation.getChainOrNull(): Chain? {
return when (this) {
is AccountDelegation.Multisig -> singleChain
is AccountDelegation.Proxy -> chain
}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_api.domain.model
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class ProxyAccount(
val proxyMetaId: Long,
val chainId: ChainId,
val proxyType: ProxyType,
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_account_api.domain.model
class SavedMultisigOperationCall(
val metaId: Long,
val chainId: String,
val callHash: ByteArray,
val callInstance: String
)
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_account_api.domain.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novasama.substrate_sdk_android.extensions.fromHex
// TODO multisig: we are using `AccountIdKey` as it logically represents the `DataByteArray`
// We need to create DataByteArray class that AccountIdKey will typealias to
typealias CallHash = AccountIdKey
fun String.intoCallHash() = fromHex().intoCallHash()
fun ByteArray.intoCallHash() = intoKey()
fun bindCallHash(decoded: Any?) = bindAccountIdKey(decoded)
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_account_api.domain.updaters
import io.novafoundation.nova.core.updater.UpdateScope
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import kotlinx.coroutines.flow.Flow
class AccountUpdateScope(
private val accountRepository: AccountRepository
) : UpdateScope<MetaAccount> {
override fun invalidationFlow(): Flow<MetaAccount> {
return accountRepository.selectedMetaAccountFlow()
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_api.domain.updaters
import io.novafoundation.nova.core.updater.UpdateScope
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
class ChainUpdateScope(
private val chainFlow: Flow<Chain>
) : UpdateScope<Chain> {
override fun invalidationFlow(): Flow<Chain> {
return chainFlow
}
}

Some files were not shown because too many files have changed in this diff Show More