Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+76
View File
@@ -0,0 +1,76 @@
apply plugin: 'kotlin-parcelize'
apply from: '../tests.gradle'
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
namespace 'io.novafoundation.nova.feature_account_impl'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"])
implementation project(':core-db')
implementation project(':common')
implementation project(':runtime')
implementation project(':feature-account-api')
implementation project(':feature-currency-api')
implementation project(':feature-ledger-api')
implementation project(':feature-ledger-core')
implementation project(':feature-versions-api')
implementation project(':feature-proxy-api')
implementation project(':feature-cloud-backup-api')
implementation project(":feature-swap-core:api")
implementation project(":feature-xcm:api")
implementation project(':web3names')
implementation project(':feature-deep-linking')
implementation kotlinDep
implementation androidDep
implementation materialDep
implementation cardViewDep
implementation constraintDep
implementation zXingCoreDep
implementation zXingEmbeddedDep
implementation bouncyCastleDep
implementation substrateSdkDep
implementation biometricDep
implementation coroutinesDep
implementation coroutinesAndroidDep
implementation viewModelKtxDep
implementation liveDataKtxDep
implementation lifeCycleKtxDep
implementation daggerDep
ksp daggerCompiler
implementation roomDep
ksp roomCompiler
implementation lifecycleDep
ksp lifecycleCompiler
testImplementation jUnitDep
testImplementation mockitoDep
implementation gsonDep
implementation insetterDep
implementation flexBoxDep
testImplementation project(":test-shared")
testImplementation project(":feature-cloud-backup-test")
}
@@ -0,0 +1 @@
<manifest />
@@ -0,0 +1,103 @@
package io.novafoundation.nova.feature_account_impl
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import io.novafoundation.nova.common.sequrity.biometry.BiometricResponse
import io.novafoundation.nova.common.sequrity.biometry.BiometricService
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.common.sequrity.biometry.BiometricPromptFactory
import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory
import kotlinx.coroutines.flow.Flow
class RealBiometricServiceFactory(
private val accountRepository: AccountRepository
) : BiometricServiceFactory {
override fun create(
biometricManager: BiometricManager,
biometricPromptFactory: BiometricPromptFactory,
promptInfo: BiometricPrompt.PromptInfo
): BiometricService {
return RealBiometricService(
accountRepository,
biometricManager,
biometricPromptFactory,
promptInfo
)
}
}
class RealBiometricService(
private val accountRepository: AccountRepository,
private val biometricManager: BiometricManager,
private val biometricPromptFactory: BiometricPromptFactory,
private val promptInfo: BiometricPrompt.PromptInfo
) : BiometricPrompt.AuthenticationCallback(), BiometricService {
override val biometryServiceResponseFlow = singleReplaySharedFlow<BiometricResponse>()
private val biometricPrompt = biometricPromptFactory.create(this)
override fun isEnabled(): Boolean {
return accountRepository.isBiometricEnabled()
}
override fun isEnabledFlow(): Flow<Boolean> = accountRepository.isBiometricEnabledFlow()
override suspend fun toggle() {
enableBiometry(!isEnabled())
}
override fun cancel() {
biometricPrompt.cancelAuthentication()
}
override fun enableBiometry(enable: Boolean) {
if (!isBiometricReady()) {
biometryServiceResponseFlow.tryEmit(BiometricResponse.NotReady)
return
}
if (enable) {
accountRepository.setBiometricOn()
} else {
accountRepository.setBiometricOff()
}
}
override fun refreshBiometryState() {
if (!isBiometricReady() && isEnabled()) {
accountRepository.setBiometricOff()
}
}
override fun requestBiometric() {
if (!isBiometricReady()) {
biometryServiceResponseFlow.tryEmit(BiometricResponse.NotReady)
return
}
biometricPrompt.authenticate(promptInfo)
}
override fun isBiometricReady(): Boolean {
return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
val cancelledByUser = errorCode == BiometricConstants.ERROR_CANCELED ||
errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricConstants.ERROR_USER_CANCELED
biometryServiceResponseFlow.tryEmit(BiometricResponse.Error(cancelledByUser, errString.toString()))
}
override fun onAuthenticationFailed() {
biometryServiceResponseFlow.tryEmit(BiometricResponse.Fail)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
biometryServiceResponseFlow.tryEmit(BiometricResponse.Success)
}
}
@@ -0,0 +1,54 @@
package io.novafoundation.nova.feature_account_impl.data.cloudBackup
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
interface CloudBackupAccountsModificationsTracker {
fun recordAccountModified(modifiedAccountTypes: List<LightMetaAccount.Type>)
fun recordAccountModified(modifiedAccountType: LightMetaAccount.Type)
fun recordAccountsModified()
fun getAccountsLastModifiedAt(): Long
}
class RealCloudBackupAccountsModificationsTracker(
private val preferences: Preferences
) : CloudBackupAccountsModificationsTracker {
init {
ensureInitialized()
}
companion object {
private const val MODIFIED_AT_KEY = "AccountsModificationsTracker.Key"
}
override fun recordAccountModified(modifiedAccountType: LightMetaAccount.Type) {
if (modifiedAccountType.isBackupable()) {
recordAccountsModified()
}
}
override fun recordAccountModified(modifiedAccountTypes: List<LightMetaAccount.Type>) {
if (modifiedAccountTypes.any { it.isBackupable() }) {
recordAccountsModified()
}
}
override fun recordAccountsModified() {
preferences.putLong(MODIFIED_AT_KEY, System.currentTimeMillis())
}
override fun getAccountsLastModifiedAt(): Long {
return preferences.getLong(MODIFIED_AT_KEY, System.currentTimeMillis())
}
private fun ensureInitialized() {
if (!preferences.contains(MODIFIED_AT_KEY)) {
recordAccountsModified()
}
}
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_account_impl.data.cloudBackup
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
fun LightMetaAccount.Type.isBackupable(): Boolean {
return when (this) {
LightMetaAccount.Type.SECRETS,
LightMetaAccount.Type.WATCH_ONLY,
LightMetaAccount.Type.PARITY_SIGNER,
LightMetaAccount.Type.LEDGER,
LightMetaAccount.Type.LEDGER_LEGACY,
LightMetaAccount.Type.POLKADOT_VAULT -> true
LightMetaAccount.Type.PROXIED,
LightMetaAccount.Type.MULTISIG -> false
}
}
@@ -0,0 +1,621 @@
package io.novafoundation.nova.feature_account_impl.data.cloudBackup
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.KeyPairSchema
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.derivationPath
import io.novafoundation.nova.common.data.secrets.v2.entropy
import io.novafoundation.nova.common.data.secrets.v2.ethereumDerivationPath
import io.novafoundation.nova.common.data.secrets.v2.ethereumKeypair
import io.novafoundation.nova.common.data.secrets.v2.keypair
import io.novafoundation.nova.common.data.secrets.v2.nonce
import io.novafoundation.nova.common.data.secrets.v2.privateKey
import io.novafoundation.nova.common.data.secrets.v2.publicKey
import io.novafoundation.nova.common.data.secrets.v2.seed
import io.novafoundation.nova.common.data.secrets.v2.substrateDerivationPath
import io.novafoundation.nova.common.data.secrets.v2.substrateKeypair
import io.novafoundation.nova.common.utils.filterNotNull
import io.novafoundation.nova.common.utils.findById
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.JoinedMetaAccountInfo
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.RelationJoinedMetaAccountInfo
import io.novafoundation.nova.feature_account_api.data.cloudBackup.CLOUD_BACKUP_APPLY_SOURCE
import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.events.buildChangesEvent
import io.novafoundation.nova.feature_account_impl.data.mappers.AccountMappers
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup.WalletPublicInfo.ChainAccountInfo.ChainAccountCryptoType
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.CloudBackupDiff
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.isEmpty
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.isNotDestructive
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.isCompletelyEmpty
import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerDerivationPath
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.ChainsById
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.chainsById
import io.novasama.substrate_sdk_android.encrypt.keypair.BaseKeypair
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.Sr25519Keypair
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.scale.EncodableStruct
class RealLocalAccountsCloudBackupFacade(
private val secretsStoreV2: SecretStoreV2,
private val accountDao: MetaAccountDao,
private val cloudBackupAccountsModificationsTracker: CloudBackupAccountsModificationsTracker,
private val metaAccountChangedEvents: MetaAccountChangesEventBus,
private val chainRegistry: ChainRegistry,
private val accountMappers: AccountMappers,
) : LocalAccountsCloudBackupFacade {
override suspend fun fullBackupInfoFromLocalSnapshot(): CloudBackup {
val allBackupableAccounts = getAllBackupableAccounts()
return CloudBackup(
publicData = allBackupableAccounts.toBackupPublicData(),
privateData = preparePrivateBackupData(allBackupableAccounts)
)
}
override suspend fun publicBackupInfoFromLocalSnapshot(): CloudBackup.PublicData {
val allBackupableAccounts = getAllBackupableAccounts()
return allBackupableAccounts.toBackupPublicData()
}
override suspend fun constructCloudBackupForFirstWallet(
metaAccount: MetaAccountLocal,
baseSecrets: EncodableStruct<MetaAccountSecrets>,
): CloudBackup {
val wrappedMetaAccount = listOf(
RelationJoinedMetaAccountInfo(
metaAccount = metaAccount,
chainAccounts = emptyList(),
proxyAccountLocal = null
)
)
val walletPrivateInfo = CloudBackup.WalletPrivateInfo(
walletId = metaAccount.globallyUniqueId,
entropy = baseSecrets.entropy,
substrate = baseSecrets.getSubstrateBackupSecrets(),
ethereum = baseSecrets.getEthereumBackupSecrets(),
chainAccounts = emptyList(),
)
return CloudBackup(
publicData = wrappedMetaAccount.toBackupPublicData(modifiedAt = System.currentTimeMillis()),
privateData = CloudBackup.PrivateData(
wallets = listOf(walletPrivateInfo)
)
)
}
override suspend fun canPerformNonDestructiveApply(diff: CloudBackupDiff): Boolean {
return diff.localChanges.isNotDestructive()
}
override suspend fun applyBackupDiff(diff: CloudBackupDiff, cloudVersion: CloudBackup) {
val localChangesToApply = diff.localChanges
if (localChangesToApply.isEmpty()) return
val metaAccountsByUuid = getAllBackupableAccounts().associateBy { it.metaAccount.globallyUniqueId }
val changesEvent = buildChangesEvent {
accountDao.runInTransaction {
addAll(applyLocalRemoval(localChangesToApply.removed, metaAccountsByUuid))
addAll(applyLocalAddition(localChangesToApply.added, cloudVersion))
addAll(
applyLocalModification(
localChangesToApply.modified,
cloudVersion,
metaAccountsByUuid
)
)
}
}
changesEvent?.let { metaAccountChangedEvents.notify(it, source = CLOUD_BACKUP_APPLY_SOURCE) }
}
private suspend fun applyLocalRemoval(
toRemove: List<CloudBackup.WalletPublicInfo>,
metaAccountsByUUid: Map<String, JoinedMetaAccountInfo>
): List<MetaAccountChangesEventBus.Event.AccountRemoved> {
if (toRemove.isEmpty()) return emptyList()
val localIds = toRemove.mapNotNull { metaAccountsByUUid[it.walletId]?.metaAccount?.id }
val allAffectedMetaAccounts = accountDao.delete(localIds)
// Clear meta account secrets
toRemove.forEach {
val localWallet = metaAccountsByUUid[it.walletId] ?: return@forEach
val chainAccountIds = localWallet.chainAccounts.map(ChainAccountLocal::accountId)
secretsStoreV2.clearMetaAccountSecrets(localWallet.metaAccount.id, chainAccountIds)
}
// Return changes
return allAffectedMetaAccounts.map {
MetaAccountChangesEventBus.Event.AccountRemoved(
metaId = it.id,
metaAccountType = accountMappers.mapMetaAccountTypeFromLocal(it.type)
)
}
}
private suspend fun applyLocalAddition(
toAdd: List<CloudBackup.WalletPublicInfo>,
cloudBackup: CloudBackup
): List<MetaAccountChangesEventBus.Event.AccountAdded> {
return toAdd.map { publicWalletInfo ->
val metaAccountLocal = publicWalletInfo.toMetaAccountLocal(
accountPosition = accountDao.nextAccountPosition(),
localIdOverwrite = null,
isSelected = false
)
val metaId = accountDao.insertMetaAccount(metaAccountLocal)
val chainAccountsLocal = publicWalletInfo.getChainAccountsLocal(metaId)
if (chainAccountsLocal.isNotEmpty()) {
accountDao.insertChainAccounts(chainAccountsLocal)
}
val metaAccountSecrets = cloudBackup.getMetaAccountSecrets(publicWalletInfo.walletId)
metaAccountSecrets?.let {
secretsStoreV2.putMetaAccountSecrets(metaId, metaAccountSecrets)
}
val chainAccountsSecrets = cloudBackup.getAllChainAccountSecrets(publicWalletInfo)
chainAccountsSecrets.forEach { (accountId, secrets) ->
secretsStoreV2.putChainAccountSecrets(metaId, accountId.value, secrets)
}
val additional = cloudBackup.getAllAdditionalSecrets(publicWalletInfo)
additional.forEach { (secretName, secretValue) ->
secretsStoreV2.putAdditionalMetaAccountSecret(metaId, secretName, secretValue)
}
MetaAccountChangesEventBus.Event.AccountAdded(
metaId = metaId,
metaAccountType = accountMappers.mapMetaAccountTypeFromLocal(metaAccountLocal.type)
)
}
}
/**
* Modification of each meta account is done in the following steps:
*
* Insert updated MetaAccountLocal
*
* Delete all previous ChainAccountLocal associated with currently processed meta account
* Insert all ChainAccountLocal from backup
*
* Update meta account secrets
* Delete all chain account secrets associated with currently processed meta account
* Insert all chain account secrets from backup
*/
private suspend fun applyLocalModification(
toModify: List<CloudBackup.WalletPublicInfo>,
cloudVersion: CloudBackup,
localMetaAccountsByUUid: Map<String, JoinedMetaAccountInfo>
): List<MetaAccountChangesEventBus.Event> {
// There seems to be some bug in Kotlin compiler which prevents us to use `return flatMap` here:
// Some internal assertion in compiler fails with error "cannot cal suspend function without continuation"
// The closest issue I have found: https://youtrack.jetbrains.com/issue/KT-48319/JVM-IR-AssertionError-FUN-caused-by-suspend-lambda-inside-anonymous-function
val result = mutableListOf<MetaAccountChangesEventBus.Event>()
toModify.onEach { publicWalletInfo ->
val oldMetaAccountJoinInfo = localMetaAccountsByUUid[publicWalletInfo.walletId] ?: return@onEach
val oldMetaAccountLocal = oldMetaAccountJoinInfo.metaAccount
val metaId = oldMetaAccountLocal.id
// Insert updated MetaAccountLocal
val updatedMetaAccountLocal = publicWalletInfo.toMetaAccountLocal(
accountPosition = oldMetaAccountLocal.position,
localIdOverwrite = metaId,
isSelected = oldMetaAccountLocal.isSelected
)
accountDao.updateMetaAccount(updatedMetaAccountLocal)
// Delete all previous ChainAccountLocal associated with currently processed meta account
if (oldMetaAccountJoinInfo.chainAccounts.isNotEmpty()) {
accountDao.deleteChainAccounts(oldMetaAccountJoinInfo.chainAccounts)
}
// Insert all ChainAccountLocal from backup
val updatedChainAccountsLocal = publicWalletInfo.getChainAccountsLocal(metaId)
if (updatedChainAccountsLocal.isNotEmpty()) {
accountDao.insertChainAccounts(updatedChainAccountsLocal)
}
// Update meta account secrets
val metaAccountSecrets = cloudVersion.getMetaAccountSecrets(publicWalletInfo.walletId)
metaAccountSecrets?.let {
secretsStoreV2.putMetaAccountSecrets(metaId, metaAccountSecrets)
}
// Delete all chain account secrets associated with currently processed meta account
val previousChainAccountIds = oldMetaAccountJoinInfo.chainAccounts.map { it.accountId }
secretsStoreV2.clearChainAccountsSecrets(metaId, previousChainAccountIds)
// Insert all chain account secrets from backup
val chainAccountsSecrets = cloudVersion.getAllChainAccountSecrets(publicWalletInfo)
chainAccountsSecrets.forEach { (accountId, secrets) ->
secretsStoreV2.putChainAccountSecrets(metaId, accountId.value, secrets)
}
val additional = cloudVersion.getAllAdditionalSecrets(publicWalletInfo)
additional.forEach { (secretName, secretValue) ->
secretsStoreV2.putAdditionalMetaAccountSecret(metaId, secretName, secretValue)
}
val metaAccountType = accountMappers.mapMetaAccountTypeFromLocal(oldMetaAccountLocal.type)
result.add(
MetaAccountChangesEventBus.Event.AccountStructureChanged(
metaId = metaId,
metaAccountType = metaAccountType
)
)
result.add(
MetaAccountChangesEventBus.Event.AccountNameChanged(
metaId = metaId,
metaAccountType = metaAccountType
)
)
}
return result
}
private fun CloudBackup.getMetaAccountSecrets(uuid: String): EncodableStruct<MetaAccountSecrets>? {
return privateData.wallets.findById(uuid)?.getLocalMetaAccountSecrets()
}
private fun CloudBackup.getAllChainAccountSecrets(walletPublicInfo: CloudBackup.WalletPublicInfo): Map<AccountIdKey, EncodableStruct<ChainAccountSecrets>> {
val privateInfo = privateData.wallets.findById(walletPublicInfo.walletId) ?: return emptyMap()
val chainAccountsSecretsByAccountId = privateInfo.chainAccounts.associateBy { it.accountId.intoKey() }
return walletPublicInfo.chainAccounts.associateBy(
keySelector = { it.accountId.intoKey() },
valueTransform = { chainAccountPublicInfo ->
val chainAccountSecrets = chainAccountsSecretsByAccountId[chainAccountPublicInfo.accountId.intoKey()]
chainAccountSecrets?.toLocalSecrets()
}
).filterNotNull()
}
private fun CloudBackup.getAllAdditionalSecrets(walletPublicInfo: CloudBackup.WalletPublicInfo): Map<String, String> {
val privateInfo = privateData.wallets.findById(walletPublicInfo.walletId) ?: return emptyMap()
val chainAccountsSecretsByAccountId = privateInfo.chainAccounts.associateBy { it.accountId.intoKey() }
fun getAllLegacyLedgerAdditionalSecrets(): Map<String, String> {
return walletPublicInfo.chainAccounts.mapNotNull { publicInfo ->
val derivationPath = chainAccountsSecretsByAccountId[publicInfo.accountId.intoKey()]?.derivationPath ?: return@mapNotNull null
val secretName = LedgerDerivationPath.legacyDerivationPathSecretKey(publicInfo.chainId)
secretName to derivationPath
}.toMap()
}
fun getAllGenericLedgerAdditionalSecrets(): Map<String, String> {
val genericDerivationPath = privateInfo.substrate!!.derivationPath!!
val secretName = LedgerDerivationPath.genericDerivationPathSecretKey()
return mapOf(secretName to genericDerivationPath)
}
return when (walletPublicInfo.type) {
CloudBackup.WalletPublicInfo.Type.LEDGER -> getAllLegacyLedgerAdditionalSecrets()
CloudBackup.WalletPublicInfo.Type.LEDGER_GENERIC -> getAllGenericLedgerAdditionalSecrets()
CloudBackup.WalletPublicInfo.Type.SECRETS,
CloudBackup.WalletPublicInfo.Type.WATCH_ONLY,
CloudBackup.WalletPublicInfo.Type.PARITY_SIGNER,
CloudBackup.WalletPublicInfo.Type.POLKADOT_VAULT -> emptyMap()
}
}
private suspend fun getAllBackupableAccounts(): List<JoinedMetaAccountInfo> {
return accountDao.getMetaAccountsByStatus(MetaAccountLocal.Status.ACTIVE)
.filter { accountMappers.mapMetaAccountTypeFromLocal(it.metaAccount.type).isBackupable() }
}
private suspend fun preparePrivateBackupData(metaAccounts: List<JoinedMetaAccountInfo>): CloudBackup.PrivateData {
return CloudBackup.PrivateData(
wallets = metaAccounts
.map { prepareWalletPrivateInfo(it) }
.filterNot { it.isCompletelyEmpty() }
)
}
private suspend fun prepareWalletPrivateInfo(joinedMetaAccountInfo: JoinedMetaAccountInfo): CloudBackup.WalletPrivateInfo {
val metaId = joinedMetaAccountInfo.metaAccount.id
val baseSecrets = secretsStoreV2.getMetaAccountSecrets(metaId)
val chainAccountsFromChainSecrets = joinedMetaAccountInfo.chainAccounts
.mapToSet { it.accountId.intoKey() } // multiple chain accounts might refer to the same account id - remove duplicates
.mapNotNull { prepareChainAccountPrivateInfo(metaId, it.value) }
val chainAccountFromAdditionalSecrets = prepareChainAccountsFromAdditionalSecrets(joinedMetaAccountInfo)
return CloudBackup.WalletPrivateInfo(
walletId = joinedMetaAccountInfo.metaAccount.globallyUniqueId,
entropy = baseSecrets?.entropy,
substrate = prepareSubstrateBackupSecrets(baseSecrets, joinedMetaAccountInfo),
ethereum = baseSecrets.getEthereumBackupSecrets(),
chainAccounts = chainAccountsFromChainSecrets + chainAccountFromAdditionalSecrets,
)
}
private suspend fun prepareSubstrateBackupSecrets(
baseSecrets: EncodableStruct<MetaAccountSecrets>?,
metaAccountLocal: JoinedMetaAccountInfo
): CloudBackup.WalletPrivateInfo.SubstrateSecrets? {
return when (metaAccountLocal.metaAccount.type) {
MetaAccountLocal.Type.LEDGER_GENERIC -> prepareGenericLedgerSubstrateBackupSecrets(metaAccountLocal)
MetaAccountLocal.Type.LEDGER,
MetaAccountLocal.Type.SECRETS,
MetaAccountLocal.Type.WATCH_ONLY,
MetaAccountLocal.Type.PARITY_SIGNER,
MetaAccountLocal.Type.POLKADOT_VAULT,
MetaAccountLocal.Type.PROXIED,
MetaAccountLocal.Type.MULTISIG -> baseSecrets.getSubstrateBackupSecrets()
}
}
private suspend fun prepareGenericLedgerSubstrateBackupSecrets(metaAccountLocal: JoinedMetaAccountInfo): CloudBackup.WalletPrivateInfo.SubstrateSecrets {
val ledgerDerivationPathKey = LedgerDerivationPath.genericDerivationPathSecretKey()
val ledgerDerivationPath = secretsStoreV2.getAdditionalMetaAccountSecret(metaAccountLocal.metaAccount.id, ledgerDerivationPathKey)
return CloudBackup.WalletPrivateInfo.SubstrateSecrets(
seed = null,
keypair = null,
derivationPath = ledgerDerivationPath
)
}
private suspend fun prepareChainAccountsFromAdditionalSecrets(
metaAccountLocal: JoinedMetaAccountInfo
): List<CloudBackup.WalletPrivateInfo.ChainAccountSecrets> {
return when (metaAccountLocal.metaAccount.type) {
MetaAccountLocal.Type.LEDGER -> prepareLegacyLedgerChainAccountSecrets(metaAccountLocal)
MetaAccountLocal.Type.LEDGER_GENERIC,
MetaAccountLocal.Type.SECRETS,
MetaAccountLocal.Type.WATCH_ONLY,
MetaAccountLocal.Type.PARITY_SIGNER,
MetaAccountLocal.Type.POLKADOT_VAULT,
MetaAccountLocal.Type.PROXIED,
MetaAccountLocal.Type.MULTISIG -> emptyList()
}
}
private suspend fun prepareLegacyLedgerChainAccountSecrets(
ledgerAccountLocal: JoinedMetaAccountInfo
): List<CloudBackup.WalletPrivateInfo.ChainAccountSecrets> {
return ledgerAccountLocal.chainAccounts.map { chainAccountLocal ->
val ledgerDerivationPathKey = LedgerDerivationPath.legacyDerivationPathSecretKey(chainAccountLocal.chainId)
val ledgerDerivationPath = secretsStoreV2.getAdditionalMetaAccountSecret(ledgerAccountLocal.metaAccount.id, ledgerDerivationPathKey)
CloudBackup.WalletPrivateInfo.ChainAccountSecrets(
accountId = chainAccountLocal.accountId,
entropy = null,
seed = null,
keypair = null,
derivationPath = ledgerDerivationPath
)
}
}
private suspend fun prepareChainAccountPrivateInfo(metaAccount: Long, chainAccountId: AccountId): CloudBackup.WalletPrivateInfo.ChainAccountSecrets? {
val secrets = secretsStoreV2.getChainAccountSecrets(metaAccount, chainAccountId) ?: return null
return secrets.toBackupSecrets(chainAccountId)
}
private fun EncodableStruct<ChainAccountSecrets>.toBackupSecrets(chainAccountId: ByteArray): CloudBackup.WalletPrivateInfo.ChainAccountSecrets {
return CloudBackup.WalletPrivateInfo.ChainAccountSecrets(
accountId = chainAccountId,
entropy = entropy,
seed = seed,
keypair = keypair.toBackupKeypairSecrets(),
derivationPath = derivationPath
)
}
private fun CloudBackup.WalletPrivateInfo.ChainAccountSecrets.toLocalSecrets(): EncodableStruct<ChainAccountSecrets>? {
return ChainAccountSecrets(
entropy = entropy,
seed = seed,
derivationPath = derivationPath,
keyPair = keypair?.toLocalKeyPair() ?: return null
)
}
private fun CloudBackup.WalletPrivateInfo.KeyPairSecrets.toLocalKeyPair(): Keypair {
val nonce = nonce
return if (nonce != null) {
Sr25519Keypair(privateKey = privateKey, publicKey = publicKey, nonce = nonce)
} else {
BaseKeypair(privateKey = privateKey, publicKey = publicKey)
}
}
private fun CloudBackup.WalletPrivateInfo.getLocalMetaAccountSecrets(): EncodableStruct<MetaAccountSecrets>? {
return MetaAccountSecrets(
entropy = entropy,
substrateSeed = substrate?.seed,
// Keypair is optional in backup since Ledger backup has base substrate derivation path but doesn't have keypair
// MetaAccountSecrets, however, require substrateKeyPair to be non-null, so we return null here in case of null keypair
// Which is a expected behavior in case of Ledger secrets
substrateKeyPair = substrate?.keypair?.toLocalKeyPair() ?: return null,
substrateDerivationPath = substrate?.derivationPath,
ethereumKeypair = ethereum?.keypair?.toLocalKeyPair(),
ethereumDerivationPath = ethereum?.derivationPath
)
}
private fun EncodableStruct<MetaAccountSecrets>?.getEthereumBackupSecrets(): CloudBackup.WalletPrivateInfo.EthereumSecrets? {
if (this == null) return null
return CloudBackup.WalletPrivateInfo.EthereumSecrets(
keypair = ethereumKeypair?.toBackupKeypairSecrets() ?: return null,
derivationPath = ethereumDerivationPath
)
}
private fun EncodableStruct<MetaAccountSecrets>?.getSubstrateBackupSecrets(): CloudBackup.WalletPrivateInfo.SubstrateSecrets? {
if (this == null) return null
return CloudBackup.WalletPrivateInfo.SubstrateSecrets(
seed = seed,
keypair = substrateKeypair.toBackupKeypairSecrets(),
derivationPath = substrateDerivationPath
)
}
private fun EncodableStruct<KeyPairSchema>.toBackupKeypairSecrets(): CloudBackup.WalletPrivateInfo.KeyPairSecrets {
return CloudBackup.WalletPrivateInfo.KeyPairSecrets(
publicKey = publicKey,
privateKey = privateKey,
nonce = nonce
)
}
private suspend fun List<JoinedMetaAccountInfo>.toBackupPublicData(
modifiedAt: Long = cloudBackupAccountsModificationsTracker.getAccountsLastModifiedAt()
): CloudBackup.PublicData {
val chainsById = chainRegistry.chainsById()
return CloudBackup.PublicData(
modifiedAt = modifiedAt,
wallets = mapNotNull { it.toBackupPublicInfo(chainsById) },
)
}
private fun JoinedMetaAccountInfo.toBackupPublicInfo(
chainsById: ChainsById,
): CloudBackup.WalletPublicInfo? {
return CloudBackup.WalletPublicInfo(
walletId = metaAccount.globallyUniqueId,
substratePublicKey = metaAccount.substratePublicKey,
substrateAccountId = metaAccount.substrateAccountId,
substrateCryptoType = metaAccount.substrateCryptoType,
ethereumAddress = metaAccount.ethereumAddress,
ethereumPublicKey = metaAccount.ethereumPublicKey,
name = metaAccount.name,
type = metaAccount.type.toBackupWalletType() ?: return null,
chainAccounts = chainAccounts.mapToSet { chainAccount -> chainAccount.toBackupPublicChainAccount(chainsById) }
)
}
private fun CloudBackup.WalletPublicInfo.toMetaAccountLocal(
accountPosition: Int,
localIdOverwrite: Long?,
isSelected: Boolean
): MetaAccountLocal {
return MetaAccountLocal(
substratePublicKey = substratePublicKey,
substrateAccountId = substrateAccountId,
substrateCryptoType = substrateCryptoType,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
name = name,
type = type.toLocalWalletType(),
globallyUniqueId = walletId,
parentMetaId = null,
isSelected = isSelected,
position = accountPosition,
status = MetaAccountLocal.Status.ACTIVE,
typeExtras = null
).also {
if (localIdOverwrite != null) {
it.id = localIdOverwrite
}
}
}
private fun CloudBackup.WalletPublicInfo.getChainAccountsLocal(metaId: Long): List<ChainAccountLocal> {
return chainAccounts.map {
ChainAccountLocal(
metaId = metaId,
chainId = it.chainId,
publicKey = it.publicKey,
accountId = it.accountId,
cryptoType = it.cryptoType?.toCryptoType()
)
}
}
private fun ChainAccountLocal.toBackupPublicChainAccount(chainsById: ChainsById): CloudBackup.WalletPublicInfo.ChainAccountInfo {
return CloudBackup.WalletPublicInfo.ChainAccountInfo(
chainId = chainId,
publicKey = publicKey,
accountId = accountId,
cryptoType = cryptoType?.toBackupChainAccountCryptoType(chainsById, chainId)
)
}
private fun MetaAccountLocal.Type.toBackupWalletType(): CloudBackup.WalletPublicInfo.Type? {
return when (this) {
MetaAccountLocal.Type.SECRETS -> CloudBackup.WalletPublicInfo.Type.SECRETS
MetaAccountLocal.Type.WATCH_ONLY -> CloudBackup.WalletPublicInfo.Type.WATCH_ONLY
MetaAccountLocal.Type.PARITY_SIGNER -> CloudBackup.WalletPublicInfo.Type.PARITY_SIGNER
MetaAccountLocal.Type.LEDGER -> CloudBackup.WalletPublicInfo.Type.LEDGER
MetaAccountLocal.Type.LEDGER_GENERIC -> CloudBackup.WalletPublicInfo.Type.LEDGER_GENERIC
MetaAccountLocal.Type.POLKADOT_VAULT -> CloudBackup.WalletPublicInfo.Type.POLKADOT_VAULT
MetaAccountLocal.Type.PROXIED, MetaAccountLocal.Type.MULTISIG -> null
}
}
private fun CloudBackup.WalletPublicInfo.Type.toLocalWalletType(): MetaAccountLocal.Type {
return when (this) {
CloudBackup.WalletPublicInfo.Type.SECRETS -> MetaAccountLocal.Type.SECRETS
CloudBackup.WalletPublicInfo.Type.WATCH_ONLY -> MetaAccountLocal.Type.WATCH_ONLY
CloudBackup.WalletPublicInfo.Type.PARITY_SIGNER -> MetaAccountLocal.Type.PARITY_SIGNER
CloudBackup.WalletPublicInfo.Type.LEDGER -> MetaAccountLocal.Type.LEDGER
CloudBackup.WalletPublicInfo.Type.LEDGER_GENERIC -> MetaAccountLocal.Type.LEDGER_GENERIC
CloudBackup.WalletPublicInfo.Type.POLKADOT_VAULT -> MetaAccountLocal.Type.POLKADOT_VAULT
}
}
private fun ChainAccountCryptoType.toCryptoType(): CryptoType {
return when (this) {
ChainAccountCryptoType.SR25519 -> CryptoType.SR25519
ChainAccountCryptoType.ED25519 -> CryptoType.ED25519
ChainAccountCryptoType.ECDSA, ChainAccountCryptoType.ETHEREUM -> CryptoType.ECDSA
}
}
private fun CryptoType.toBackupChainAccountCryptoType(chainsById: ChainsById, chainId: ChainId): ChainAccountCryptoType? {
val isEvm = chainsById.isEVM(chainId) ?: return null
if (isEvm) return ChainAccountCryptoType.ETHEREUM
return when (this) {
CryptoType.SR25519 -> ChainAccountCryptoType.SR25519
CryptoType.ED25519 -> ChainAccountCryptoType.ED25519
CryptoType.ECDSA -> ChainAccountCryptoType.ECDSA
}
}
private fun ChainsById.isEVM(chainId: ChainId): Boolean? {
return get(chainId)?.isEthereumBased
}
}
@@ -0,0 +1,205 @@
package io.novafoundation.nova.feature_account_impl.data.ethereum.transaction
import io.novafoundation.nova.common.utils.castOrNull
import io.novafoundation.nova.common.utils.toEcdsaSignatureData
import io.novafoundation.nova.core.ethereum.Web3Api
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EthereumTransactionExecution
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionBuilding
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin
import io.novafoundation.nova.feature_account_api.data.model.EvmFee
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.requireMetaAccountFor
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn
import io.novafoundation.nova.runtime.ethereum.EvmRpcException
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory
import io.novafoundation.nova.runtime.ethereum.sendSuspend
import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder
import io.novafoundation.nova.runtime.ext.commissionAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import org.web3j.crypto.RawTransaction
import org.web3j.crypto.Sign
import org.web3j.crypto.TransactionEncoder
import org.web3j.protocol.core.DefaultBlockParameterName
import org.web3j.protocol.core.methods.request.Transaction
import org.web3j.rlp.RlpEncoder
import org.web3j.rlp.RlpList
import java.math.BigInteger
import kotlinx.coroutines.delay
import org.web3j.protocol.core.methods.response.TransactionReceipt
import kotlin.String
import kotlin.time.Duration.Companion.seconds
internal class RealEvmTransactionService(
private val accountRepository: AccountRepository,
private val chainRegistry: ChainRegistry,
private val signerProvider: SignerProvider,
private val gasPriceProviderFactory: GasPriceProviderFactory,
) : EvmTransactionService {
override suspend fun calculateFee(
chainId: ChainId,
origin: TransactionOrigin,
fallbackGasLimit: BigInteger,
building: EvmTransactionBuilding
): Fee {
val web3Api = chainRegistry.getCallEthereumApiOrThrow(chainId)
val chain = chainRegistry.getChain(chainId)
val submittingMetaAccount = accountRepository.requireMetaAccountFor(origin, chainId)
val submittingAddress = submittingMetaAccount.requireAddressIn(chain)
val txBuilder = EvmTransactionBuilder().apply(building)
val txForFee = txBuilder.buildForFee(submittingAddress)
val gasPrice = gasPriceProviderFactory.createKnown(chainId).getGasPrice()
val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit)
return EvmFee(
gasLimit,
gasPrice,
SubmissionOrigin.singleOrigin(submittingMetaAccount.requireAccountIdIn(chain)),
chain.commissionAsset
)
}
override suspend fun transact(
chainId: ChainId,
presetFee: Fee?,
origin: TransactionOrigin,
fallbackGasLimit: BigInteger,
building: EvmTransactionBuilding
): Result<ExtrinsicSubmission> = runCatching {
val chain = chainRegistry.getChain(chainId)
val submittingMetaAccount = accountRepository.requireMetaAccountFor(origin, chainId)
val submittingAddress = submittingMetaAccount.requireAddressIn(chain)
val submittingAccountId = submittingMetaAccount.requireAccountIdIn(chain)
val web3Api = chainRegistry.getCallEthereumApiOrThrow(chainId)
val txBuilder = EvmTransactionBuilder().apply(building)
val evmFee = presetFee?.castOrNull<EvmFee>() ?: run {
val txForFee = txBuilder.buildForFee(submittingAddress)
val gasPrice = gasPriceProviderFactory.createKnown(chainId).getGasPrice()
val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit)
EvmFee(
gasLimit,
gasPrice,
SubmissionOrigin.singleOrigin(submittingAccountId),
chain.commissionAsset
)
}
val nonce = web3Api.getNonce(submittingAddress)
val txForSign = txBuilder.buildForSign(nonce = nonce, gasPrice = evmFee.gasPrice, gasLimit = evmFee.gasLimit)
val toSubmit = signTransaction(txForSign, submittingMetaAccount, chain)
val txHash = web3Api.sendTransaction(toSubmit)
val callExecutionType = CallExecutionType.IMMEDIATE
ExtrinsicSubmission(
hash = txHash,
submissionOrigin = SubmissionOrigin.singleOrigin(submittingAccountId),
// Well, actually some smart-contracts might be "delayed", e.g. Gnosis Multisigs
// But we don't care at this point since this service is used for internal app txs only, basically just for the transfers
callExecutionType = callExecutionType,
submissionHierarchy = SubmissionHierarchy(submittingMetaAccount, callExecutionType)
)
}
private suspend fun signTransaction(txForSign: RawTransaction, metaAccount: MetaAccount, chain: Chain): String {
val ethereumChainId = chain.addressPrefix.toLong()
val encodedTx = TransactionEncoder.encode(txForSign, ethereumChainId)
val signer = signerProvider.rootSignerFor(metaAccount)
val accountId = metaAccount.requireAccountIdIn(chain)
val signerPayload = SignerPayloadRaw(encodedTx, accountId)
val signatureData = signer.signRaw(signerPayload).toEcdsaSignatureData()
val eip155SignatureData: Sign.SignatureData = TransactionEncoder.createEip155SignatureData(signatureData, ethereumChainId)
return txForSign.encodeWith(eip155SignatureData).toHexString(withPrefix = true)
}
override suspend fun transactAndAwaitExecution(
chainId: ChainId,
presetFee: Fee?,
origin: TransactionOrigin,
fallbackGasLimit: BigInteger,
building: EvmTransactionBuilding
): Result<EthereumTransactionExecution> {
return transact(chainId, presetFee, origin, fallbackGasLimit, building)
.mapCatching {
val transactionReceipt = it.awaitExecution(chainId)
EthereumTransactionExecution(
extrinsicHash = transactionReceipt.transactionHash,
blockHash = transactionReceipt.blockHash,
it.submissionHierarchy
)
}
}
private suspend fun ExtrinsicSubmission.awaitExecution(chainId: ChainId): TransactionReceipt {
val deadline = System.currentTimeMillis() + 60.seconds.inWholeMilliseconds
while (true) {
val web3Api = chainRegistry.getCallEthereumApiOrThrow(chainId)
val response = web3Api.ethGetTransactionReceipt(hash).sendSuspend()
val optionalReceipt = response.transactionReceipt
if (optionalReceipt.isPresent) {
val receipt = optionalReceipt.get()
if (!receipt.isStatusOK()) {
throw RuntimeException("EVM tx reverted: $hash")
}
return receipt
}
if (System.currentTimeMillis() > deadline) {
throw RuntimeException("Timeout while waiting for tx: $hash")
}
delay(3.seconds.inWholeMilliseconds)
}
}
private suspend fun Web3Api.getNonce(address: String): BigInteger {
return ethGetTransactionCount(address, DefaultBlockParameterName.PENDING)
.sendSuspend()
.transactionCount
}
private suspend fun Web3Api.gasLimitOrDefault(tx: Transaction, default: BigInteger): BigInteger = try {
ethEstimateGas(tx).sendSuspend().amountUsed
} catch (rpcException: EvmRpcException) {
default
}
private fun RawTransaction.encodeWith(signatureData: Sign.SignatureData): ByteArray {
val values = TransactionEncoder.asRlpValues(this, signatureData)
val rlpList = RlpList(values)
return RlpEncoder.encode(rlpList)
}
private suspend fun Web3Api.sendTransaction(transactionData: String): String {
return ethSendRawTransaction(transactionData).sendSuspend().transactionHash
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_account_impl.data.events
import io.novafoundation.nova.common.utils.bus.BaseEventBus
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.events.allAffectedMetaAccountTypes
import io.novafoundation.nova.feature_account_api.data.externalAccounts.ExternalAccountsSyncService
import io.novafoundation.nova.feature_account_impl.data.cloudBackup.CloudBackupAccountsModificationsTracker
/**
* Implementation of [MetaAccountChangesEventBus] that also performs some additional action known to account feature
* Components from external modules can subscribe to this event bus on the upper level
*/
class RealMetaAccountChangesEventBus(
private val externalAccountsSyncService: dagger.Lazy<ExternalAccountsSyncService>,
private val cloudBackupAccountsModificationsTracker: CloudBackupAccountsModificationsTracker
) : BaseEventBus<MetaAccountChangesEventBus.Event>(), MetaAccountChangesEventBus {
override suspend fun notify(event: MetaAccountChangesEventBus.Event, source: String?) {
super.notify(event, source)
cloudBackupAccountsModificationsTracker.recordAccountModified(event.allAffectedMetaAccountTypes())
externalAccountsSyncService.get().syncOnAccountChange(event, source)
}
}
@@ -0,0 +1,164 @@
package io.novafoundation.nova.feature_account_impl.data.extrinsic
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.data.network.runtime.binding.fitsIn
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.common.utils.min
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSplitter
import io.novafoundation.nova.feature_account_api.data.extrinsic.SplitCalls
import io.novafoundation.nova.runtime.ext.requireGenesisHash
import io.novafoundation.nova.runtime.extrinsic.CustomTransactionExtensions
import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.network.binding.BlockWeightLimits
import io.novafoundation.nova.runtime.network.binding.PerDispatchClassWeight
import io.novafoundation.nova.runtime.network.binding.total
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
import io.novafoundation.nova.runtime.repository.BlockLimitsRepository
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode
import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicVersion
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.ChargeTransactionPayment
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckGenesis
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckMortality
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckSpecVersion
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckTxVersion
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.checkMetadataHash.CheckMetadataHash
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.checkMetadataHash.CheckMetadataHashMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.math.BigInteger
import javax.inject.Inject
private typealias CallWeightsByType = Map<String, Deferred<WeightV2>>
private const val LEAVE_SOME_SPACE_MULTIPLIER = 0.8
@FeatureScope
internal class RealExtrinsicSplitter @Inject constructor(
private val rpcCalls: RpcCalls,
private val blockLimitsRepository: BlockLimitsRepository,
private val signingContextFactory: SigningContext.Factory,
private val chainRegistry: ChainRegistry,
) : ExtrinsicSplitter {
override suspend fun split(signer: NovaSigner, callBuilder: CallBuilder, chain: Chain): SplitCalls = coroutineScope {
val weightByCallId = estimateWeightByCallType(signer, callBuilder, chain)
val blockLimit = blockLimitsRepository.blockLimits(chain.id)
val lastBlockWeight = blockLimitsRepository.lastBlockWeight(chain.id)
val extrinsicLimit = determineExtrinsicLimit(blockLimit, lastBlockWeight)
val signerLimit = signer.maxCallsPerTransaction()
callBuilder.splitCallsWith(weightByCallId, extrinsicLimit, signerLimit)
}
override suspend fun estimateCallWeight(signer: NovaSigner, call: GenericCall.Instance, chain: Chain): WeightV2 {
val runtime = chainRegistry.getRuntime(chain.id)
val fakeExtrinsic = wrapInFakeExtrinsic(signer, call, runtime, chain)
return rpcCalls.getExtrinsicFee(chain, fakeExtrinsic).weight
}
private fun determineExtrinsicLimit(blockLimits: BlockWeightLimits, lastBlockWeight: PerDispatchClassWeight): WeightV2 {
val extrinsicLimit = blockLimits.perClass.normal.maxExtrinsic
val normalClassLimit = blockLimits.perClass.normal.maxTotal - lastBlockWeight.normal
val blockLimit = blockLimits.maxBlock - lastBlockWeight.total()
val unionLimit = min(extrinsicLimit, normalClassLimit, blockLimit)
return unionLimit * LEAVE_SOME_SPACE_MULTIPLIER
}
private val GenericCall.Instance.uniqueId: String
get() {
val (moduleIdx, functionIdx) = function.index
return "$moduleIdx:$functionIdx"
}
@Suppress("SuspendFunctionOnCoroutineScope")
private suspend fun CoroutineScope.estimateWeightByCallType(signer: NovaSigner, callBuilder: CallBuilder, chain: Chain): CallWeightsByType {
return callBuilder.calls.groupBy { it.uniqueId }
.mapValues { (_, calls) ->
val sample = calls.first()
val sampleExtrinsic = wrapInFakeExtrinsic(signer, sample, callBuilder.runtime, chain)
async { rpcCalls.getExtrinsicFee(chain, sampleExtrinsic).weight }
}
}
private suspend fun CallBuilder.splitCallsWith(
weights: CallWeightsByType,
blockWeightLimit: WeightV2,
signerNumberOfCallsLimit: Int?,
): SplitCalls {
val split = mutableListOf<List<GenericCall.Instance>>()
var currentBatch = mutableListOf<GenericCall.Instance>()
var currentBatchWeight: WeightV2 = WeightV2.zero()
calls.forEach { call ->
val estimatedCallWeight = weights.getValue(call.uniqueId).await()
val newWeight = currentBatchWeight + estimatedCallWeight
val exceedsByWeight = !newWeight.fitsIn(blockWeightLimit)
val exceedsByNumberOfCalls = signerNumberOfCallsLimit != null && currentBatch.size >= signerNumberOfCallsLimit
if (exceedsByWeight || exceedsByNumberOfCalls) {
if (!estimatedCallWeight.fitsIn(blockWeightLimit)) throw IllegalArgumentException("Impossible to fit call $call into a block")
split += currentBatch
currentBatchWeight = estimatedCallWeight
currentBatch = mutableListOf(call)
} else {
currentBatchWeight += estimatedCallWeight
currentBatch += call
}
}
if (currentBatch.isNotEmpty()) {
split.add(currentBatch)
}
return split
}
private suspend fun wrapInFakeExtrinsic(
signer: NovaSigner,
call: GenericCall.Instance,
runtime: RuntimeSnapshot,
chain: Chain
): SendableExtrinsic {
val genesisHash = chain.requireGenesisHash().fromHex()
return ExtrinsicBuilder(
runtime = runtime,
extrinsicVersion = ExtrinsicVersion.V4,
batchMode = BatchMode.BATCH,
).apply {
setTransactionExtension(CheckMortality(Era.Immortal, genesisHash))
setTransactionExtension(CheckGenesis(chain.requireGenesisHash().fromHex()))
setTransactionExtension(ChargeTransactionPayment(BigInteger.ZERO))
setTransactionExtension(CheckMetadataHash(CheckMetadataHashMode.Disabled))
setTransactionExtension(CheckSpecVersion(0))
setTransactionExtension(CheckTxVersion(0))
CustomTransactionExtensions.defaultValues(runtime).forEach(::setTransactionExtension)
call(call)
val signingContext = signingContextFactory.default(chain)
signer.setSignerDataForFee(signingContext)
}.buildExtrinsic()
}
}
@@ -0,0 +1,436 @@
package io.novafoundation.nova.feature_account_impl.data.extrinsic
import android.util.Log
import io.novafoundation.nova.common.data.network.runtime.binding.DispatchError
import io.novafoundation.nova.common.data.network.runtime.binding.bindDispatchError
import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.mapAsync
import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult
import io.novafoundation.nova.common.utils.multiResult.runMultiCatching
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.common.utils.provideContext
import io.novafoundation.nova.common.utils.takeWhileInclusive
import io.novafoundation.nova.common.utils.tip
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicBuildingContext
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService.SubmissionOptions
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSplitter
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
import io.novafoundation.nova.feature_account_api.data.extrinsic.FormExtrinsicWithOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsicWithOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicDispatch
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.FeePayment
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
import io.novafoundation.nova.feature_account_api.data.fee.toChainAsset
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
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.SubmissionHierarchy
import io.novafoundation.nova.feature_account_api.data.signer.SigningMode
import io.novafoundation.nova.feature_account_api.data.signer.setSignerData
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.requireMetaAccountFor
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.feature_account_impl.data.signer.signingContext.withSequenceSigning
import io.novafoundation.nova.runtime.ext.commissionAsset
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
import io.novafoundation.nova.runtime.extrinsic.multi.SimpleCallBuilder
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findExtrinsicFailureOrThrow
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.isSuccess
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
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.GenericEvent
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
class RealExtrinsicService(
private val rpcCalls: RpcCalls,
private val chainRegistry: ChainRegistry,
private val accountRepository: AccountRepository,
private val extrinsicBuilderFactory: ExtrinsicBuilderFactory,
private val signerProvider: SignerProvider,
private val extrinsicSplitter: ExtrinsicSplitter,
private val feePaymentProviderRegistry: FeePaymentProviderRegistry,
private val eventsRepository: EventsRepository,
private val signingContextFactory: SigningContext.Factory,
private val coroutineScope: CoroutineScope? // TODO: Make it non-nullable
) : ExtrinsicService {
override suspend fun submitExtrinsic(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormExtrinsicWithOrigin
): Result<ExtrinsicSubmission> = runCatching {
val (extrinsic, submissionOrigin, _, callExecutionType, signingHierarchy) = buildSubmissionExtrinsic(chain, origin, formExtrinsic, submissionOptions)
val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic)
ExtrinsicSubmission(hash, submissionOrigin, callExecutionType, signingHierarchy)
}
override suspend fun submitMultiExtrinsicAwaitingInclusion(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormMultiExtrinsicWithOrigin
): RetriableMultiResult<ExtrinsicWatchResult<ExtrinsicStatus.InBlock>> {
return runMultiCatching(
intermediateListLoading = {
val submission = constructSplitExtrinsics(chain, origin, formExtrinsic, submissionOptions, SigningMode.SUBMISSION)
submission.extrinsics.map { it to submission }
},
listProcessing = { (extrinsic, submission) ->
rpcCalls.submitAndWatchExtrinsic(chain.id, extrinsic)
.filterIsInstance<ExtrinsicStatus.InBlock>()
.map { ExtrinsicWatchResult(it, submission.submissionHierarchy) }
.first()
}
)
}
// TODO: The flow in Result may produce an exception that will be not handled since Result can't catch an exception inside a flow
// For now it's handled in awaitInBlock() extension
override suspend fun submitAndWatchExtrinsic(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormExtrinsicWithOrigin
): Result<Flow<ExtrinsicWatchResult<ExtrinsicStatus>>> = runCatching {
val singleSubmission = buildSubmissionExtrinsic(chain, origin, formExtrinsic, submissionOptions)
rpcCalls.submitAndWatchExtrinsic(chain.id, singleSubmission.extrinsic)
.map { ExtrinsicWatchResult(it, singleSubmission.submissionHierarchy) }
.takeWhileInclusive { !it.status.terminal }
}
override suspend fun submitExtrinsicAndAwaitExecution(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormExtrinsicWithOrigin
): Result<ExtrinsicExecutionResult> {
return submitAndWatchExtrinsic(chain, origin, submissionOptions, formExtrinsic)
.awaitInBlock()
.map { determineExtrinsicOutcome(it, chain) }
}
override suspend fun paymentInfo(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormExtrinsicWithOrigin
): FeeResponse {
val (extrinsic) = buildFeeExtrinsic(chain, origin, formExtrinsic, submissionOptions)
return rpcCalls.getExtrinsicFee(chain, extrinsic)
}
override suspend fun estimateFee(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormExtrinsicWithOrigin
): Fee {
val (extrinsic, submissionOrigin, feePayment) = buildFeeExtrinsic(chain, origin, formExtrinsic, submissionOptions)
val nativeFee = estimateNativeFee(chain, extrinsic, submissionOrigin)
return feePayment.convertNativeFee(nativeFee)
}
override suspend fun estimateFee(
chain: Chain,
extrinsic: String,
usedSigner: NovaSigner,
): Fee {
val runtime = chainRegistry.getRuntime(chain.id)
val sendableExtrinsic = SendableExtrinsic(runtime, Extrinsic.fromHex(runtime, extrinsic))
val submissionOrigin = usedSigner.submissionOrigin(chain)
val nativeFee = estimateNativeFee(chain, sendableExtrinsic, submissionOrigin)
val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id)
val feePayment = feePaymentProvider.detectFeePaymentFromExtrinsic(sendableExtrinsic)
return feePayment.convertNativeFee(nativeFee)
}
override suspend fun estimateMultiFee(
chain: Chain,
origin: TransactionOrigin,
submissionOptions: SubmissionOptions,
formExtrinsic: FormMultiExtrinsicWithOrigin
): Fee {
val (extrinsics, submissionOrigin) = constructSplitExtrinsics(chain, origin, formExtrinsic, submissionOptions, SigningMode.FEE)
require(extrinsics.isNotEmpty()) { "Empty extrinsics list" }
val fees = extrinsics.mapAsync { estimateNativeFee(chain, it, submissionOrigin) }
val totalFeeAmount = fees.sumOf { it.amount }
val totalNativeFee = SubstrateFee(
amount = totalFeeAmount,
submissionOrigin = submissionOrigin,
asset = submissionOptions.feePaymentCurrency.toChainAsset(chain)
)
val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id)
val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope)
return feePayment.convertNativeFee(totalNativeFee)
}
private suspend fun determineExtrinsicOutcome(
watchResult: ExtrinsicWatchResult<ExtrinsicStatus.InBlock>,
chain: Chain
): ExtrinsicExecutionResult {
val status = watchResult.status
val outcome = runCatching {
val extrinsicWithEvents = eventsRepository.getExtrinsicWithEvents(chain.id, status.extrinsicHash, status.blockHash)
val runtime = chainRegistry.getRuntime(chain.id)
requireNotNull(extrinsicWithEvents) {
"No extrinsic included into expected block"
}
extrinsicWithEvents.determineOutcome(runtime)
}.getOrElse {
Log.w(LOG_TAG, "Failed to determine extrinsic outcome", it)
ExtrinsicDispatch.Unknown
}
return ExtrinsicExecutionResult(
extrinsicHash = status.extrinsicHash,
blockHash = status.blockHash,
outcome = outcome,
submissionHierarchy = watchResult.submissionHierarchy
)
}
private fun ExtrinsicWithEvents.determineOutcome(runtimeSnapshot: RuntimeSnapshot): ExtrinsicDispatch {
return if (isSuccess()) {
ExtrinsicDispatch.Ok(events)
} else {
val errorEvent = events.findExtrinsicFailureOrThrow()
val dispatchError = parseErrorEvent(errorEvent, runtimeSnapshot)
ExtrinsicDispatch.Failed(dispatchError)
}
}
private fun parseErrorEvent(errorEvent: GenericEvent.Instance, runtimeSnapshot: RuntimeSnapshot): DispatchError {
val dispatchError = errorEvent.arguments.first()
return runtimeSnapshot.provideContext { bindDispatchError(dispatchError) }
}
private suspend fun constructSplitExtrinsics(
chain: Chain,
origin: TransactionOrigin,
formExtrinsic: FormMultiExtrinsicWithOrigin,
submissionOptions: SubmissionOptions,
signingMode: SigningMode
): MultiSubmission {
val signer = getSigner(chain, origin)
val extrinsicBuilderSequence = extrinsicBuilderFactory.createMulti(
chain = chain,
options = submissionOptions.toBuilderFactoryOptions()
)
val runtime = chainRegistry.getRuntime(chain.id)
val submissionOrigin = signer.submissionOrigin(chain)
val buildingContext = ExtrinsicBuildingContext(submissionOrigin, signer, chain)
val callBuilder = SimpleCallBuilder(runtime).apply { formExtrinsic(buildingContext) }
val splitCalls = extrinsicSplitter.split(signer, callBuilder, chain)
val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id)
val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope)
val extrinsicBuilderIterator = extrinsicBuilderSequence.iterator()
// Setup signing
val signingContext = signingContextFactory.default(chain).withSequenceSigning()
val extrinsics = splitCalls.map { batch ->
// Create empty builder
val extrinsicBuilder = extrinsicBuilderIterator.next()
// Add upstream calls
batch.forEach(extrinsicBuilder::call)
// Setup fees
feePayment.modifyExtrinsic(extrinsicBuilder)
// Setup signing
with(extrinsicBuilder) {
signer.setSignerData(signingContext, signingMode)
}
// Build extrinsic
extrinsicBuilder.buildExtrinsic().also {
signingContext.incrementNonceOffset()
}
}
val signingHierarchy = signer.getSigningHierarchy()
return MultiSubmission(extrinsics, submissionOrigin, feePayment, signingHierarchy)
}
private suspend fun buildSubmissionExtrinsic(
chain: Chain,
origin: TransactionOrigin,
formExtrinsic: FormExtrinsicWithOrigin,
submissionOptions: SubmissionOptions,
): SingleSubmission {
return buildExtrinsic(chain, origin, formExtrinsic, submissionOptions, SigningMode.SUBMISSION)
}
private suspend fun buildFeeExtrinsic(
chain: Chain,
origin: TransactionOrigin,
formExtrinsic: FormExtrinsicWithOrigin,
submissionOptions: SubmissionOptions,
): SingleSubmission {
return buildExtrinsic(chain, origin, formExtrinsic, submissionOptions, SigningMode.FEE)
}
private suspend fun buildExtrinsic(
chain: Chain,
origin: TransactionOrigin,
formExtrinsic: FormExtrinsicWithOrigin,
submissionOptions: SubmissionOptions,
signingMode: SigningMode
): SingleSubmission {
val signer = getSigner(chain, origin)
val submissionOrigin = signer.submissionOrigin(chain)
// Create empty builder
val extrinsicBuilder = extrinsicBuilderFactory.create(
chain = chain,
options = submissionOptions.toBuilderFactoryOptions()
)
// Add upstream calls
val buildingContext = ExtrinsicBuildingContext(submissionOrigin, signer, chain)
extrinsicBuilder.formExtrinsic(buildingContext)
// Setup fees
val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id)
val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope)
feePayment.modifyExtrinsic(extrinsicBuilder)
// Setup signing
val signingContext = signingContextFactory.default(chain)
with(extrinsicBuilder) {
signer.setSignerData(signingContext, signingMode)
}
// Build extrinsic
val extrinsic = try {
Log.d("RealExtrinsicService", "Building extrinsic for chain ${chain.name} (${chain.id})")
extrinsicBuilder.buildExtrinsic()
} catch (e: Exception) {
Log.e("RealExtrinsicService", "Failed to build extrinsic for chain ${chain.name}", e)
Log.e("RealExtrinsicService", "SigningMode: $signingMode, Chain: ${chain.id}")
Log.e("RealExtrinsicService", "Exception class: ${e::class.java.name}")
Log.e("RealExtrinsicService", "Message: ${e.message}")
Log.e("RealExtrinsicService", "Cause: ${e.cause?.message}")
Log.e("RealExtrinsicService", "Full stack trace:", e)
// Get runtime diagnostics
try {
val runtime = chainRegistry.getRuntime(chain.id)
val typeRegistry = runtime.typeRegistry
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
val hasMultiSignature = typeRegistry["MultiSignature"] != null
val hasMultiAddress = typeRegistry["MultiAddress"] != null
val hasAddress = typeRegistry["Address"] != null
Log.e(
"RealExtrinsicService",
"Types: ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, " +
"MultiAddress=$hasMultiAddress, Address=$hasAddress"
)
// Check extrinsic extensions
val signedExtensions = runtime.metadata.extrinsic.signedExtensions.map { it.id }
Log.e("RealExtrinsicService", "Signed extensions: $signedExtensions")
} catch (diagEx: Exception) {
Log.e("RealExtrinsicService", "Failed to get diagnostics: ${diagEx.message}")
}
throw e
}
val signingHierarchy = signer.getSigningHierarchy()
return SingleSubmission(extrinsic, submissionOrigin, feePayment, signer.callExecutionType(), signingHierarchy)
}
private fun SubmissionOptions.toBuilderFactoryOptions(): ExtrinsicBuilderFactory.Options {
return ExtrinsicBuilderFactory.Options(batchMode)
}
private suspend fun getSigner(chain: Chain, origin: TransactionOrigin): NovaSigner {
val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id)
return signerProvider.rootSignerFor(metaAccount)
}
private data class SingleSubmission(
val extrinsic: SendableExtrinsic,
val submissionOrigin: SubmissionOrigin,
val feePayment: FeePayment,
val callExecutionType: CallExecutionType,
val submissionHierarchy: SubmissionHierarchy,
)
private data class MultiSubmission(
val extrinsics: List<SendableExtrinsic>,
val submissionOrigin: SubmissionOrigin,
val feePayment: FeePayment,
val submissionHierarchy: SubmissionHierarchy
)
private suspend fun NovaSigner.submissionOrigin(chain: Chain): SubmissionOrigin {
val executingAccount = metaAccount.requireAccountIdIn(chain)
val signingAccount = submissionSignerAccountId(chain)
return SubmissionOrigin(executingAccount, signingAccount)
}
private suspend fun estimateNativeFee(
chain: Chain,
sendableExtrinsic: SendableExtrinsic,
submissionOrigin: SubmissionOrigin
): Fee {
val baseFee = rpcCalls.getExtrinsicFee(chain, sendableExtrinsic).partialFee
val tip = sendableExtrinsic.extrinsic.tip().orZero()
return SubstrateFee(
amount = tip + baseFee,
submissionOrigin = submissionOrigin,
chain.commissionAsset
)
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_account_impl.data.extrinsic
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.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
class RealExtrinsicServiceFactory(
private val rpcCalls: RpcCalls,
private val chainRegistry: ChainRegistry,
private val accountRepository: AccountRepository,
private val extrinsicBuilderFactory: ExtrinsicBuilderFactory,
private val signerProvider: SignerProvider,
private val extrinsicSplitter: ExtrinsicSplitter,
private val eventsRepository: EventsRepository,
private val feePaymentProviderRegistry: FeePaymentProviderRegistry,
private val signingContextFactory: SigningContext.Factory,
) : ExtrinsicService.Factory {
override fun create(feeConfig: ExtrinsicService.FeePaymentConfig): ExtrinsicService {
val registry = getRegistry(feeConfig)
return RealExtrinsicService(
rpcCalls = rpcCalls,
chainRegistry = chainRegistry,
accountRepository = accountRepository,
extrinsicBuilderFactory = extrinsicBuilderFactory,
signerProvider = signerProvider,
extrinsicSplitter = extrinsicSplitter,
feePaymentProviderRegistry = registry,
eventsRepository = eventsRepository,
coroutineScope = feeConfig.coroutineScope,
signingContextFactory = signingContextFactory
)
}
private fun getRegistry(config: ExtrinsicService.FeePaymentConfig): FeePaymentProviderRegistry {
return config.customFeePaymentRegistry ?: feePaymentProviderRegistry
}
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_impl.data.fee
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProvider
import io.novafoundation.nova.feature_account_impl.data.fee.chains.DefaultFeePaymentProvider
import io.novafoundation.nova.feature_account_impl.data.fee.chains.HydrationFeePaymentProvider
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
internal class RealFeePaymentProviderRegistry(
private val assetHubFactory: AssetHubFeePaymentProvider.Factory,
private val hydrationFactory: HydrationFeePaymentProvider.Factory,
private val chainRegistry: ChainRegistry,
) : FeePaymentProviderRegistry {
override suspend fun providerFor(chainId: ChainId): FeePaymentProvider {
val chain = chainRegistry.getChain(chainId)
return when (chainId) {
Chain.Geneses.PEZKUWI_ASSET_HUB,
Chain.Geneses.POLKADOT_ASSET_HUB,
Chain.Geneses.KUSAMA_ASSET_HUB -> assetHubFactory.create(chain)
Chain.Geneses.HYDRA_DX -> hydrationFactory.create(chain)
else -> DefaultFeePaymentProvider(chain)
}
}
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_account_impl.data.fee.capability
import android.util.Log
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.fee.capability.CustomFeeCapabilityFacade
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.requestedAccountPaysFees
class RealCustomCustomFeeCapabilityFacade(
private val accountRepository: AccountRepository,
private val feePaymentProviderRegistry: FeePaymentProviderRegistry
) : CustomFeeCapabilityFacade {
override suspend fun canPayFeeInCurrency(currency: FeePaymentCurrency): Boolean {
return when (currency) {
is FeePaymentCurrency.Asset -> canPayFeeInNonUtilityAsset(currency)
FeePaymentCurrency.Native -> true
}
}
private suspend fun canPayFeeInNonUtilityAsset(
currency: FeePaymentCurrency.Asset,
): Boolean {
if (hasGlobalFeePaymentRestrictions()) return false
return feePaymentProviderRegistry.providerFor(currency.asset.chainId)
.canPayFee(currency)
.onFailure { Log.e("RealCustomCustomFeeCapabilityFacade", "Failed to check canPayFee", it) }
.getOrDefault(false)
}
override suspend fun hasGlobalFeePaymentRestrictions(): Boolean {
val currentMetaAccount = accountRepository.getSelectedMetaAccount()
return !currentMetaAccount.type.requestedAccountPaysFees()
}
}
@@ -0,0 +1,77 @@
package io.novafoundation.nova.feature_account_impl.data.fee.chains
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.novafoundation.nova.feature_account_api.data.fee.FeePayment
import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability
import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider
import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment
import io.novafoundation.nova.feature_account_api.data.fee.types.assetHub.findChargeAssetTxPayment
import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetConversionFeePayment
import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetHubFastLookupFeeCapability
import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetHubFeePaymentAssetsFetcherFactory
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import kotlinx.coroutines.CoroutineScope
class AssetHubFeePaymentProvider @AssistedInject constructor(
@Assisted override val chain: Chain,
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
private val multiLocationConverterFactory: MultiLocationConverterFactory,
private val assetHubFeePaymentAssetsFetcher: AssetHubFeePaymentAssetsFetcherFactory,
private val xcmVersionDetector: XcmVersionDetector
) : CustomOrNativeFeePaymentProvider() {
@AssistedFactory
interface Factory {
fun create(chain: Chain): AssetHubFeePaymentProvider
}
override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment {
val multiLocationConverter = multiLocationConverterFactory.defaultSync(chain)
return AssetConversionFeePayment(
paymentAsset = customFeeAsset,
multiChainRuntimeCallsApi = multiChainRuntimeCallsApi,
multiLocationConverter = multiLocationConverter,
xcmVersionDetector = xcmVersionDetector
)
}
override suspend fun canPayFeeInNonUtilityToken(customFeeAsset: Chain.Asset): Result<Boolean> {
// Asset hub does not support per-asset optimized query
return fastLookupCustomFeeCapability()
.map { it.canPayFeeInNonUtilityToken(customFeeAsset.id) }
}
override suspend fun detectFeePaymentFromExtrinsic(extrinsic: SendableExtrinsic): FeePayment {
val multiLocationConverter = multiLocationConverterFactory.defaultSync(chain)
val feePaymentAsset = extrinsic.extrinsic.detectFeePaymentAsset(multiLocationConverter) ?: return NativeFeePayment()
return AssetConversionFeePayment(
paymentAsset = feePaymentAsset,
multiChainRuntimeCallsApi = multiChainRuntimeCallsApi,
multiLocationConverter = multiLocationConverter,
xcmVersionDetector = xcmVersionDetector
)
}
override suspend fun fastLookupCustomFeeCapability(): Result<FastLookupCustomFeeCapability> {
return runCatching {
val fetcher = assetHubFeePaymentAssetsFetcher.create(chain)
AssetHubFastLookupFeeCapability(fetcher.fetchAvailablePaymentAssets())
}
}
private suspend fun Extrinsic.Instance.detectFeePaymentAsset(multiLocationConverter: MultiLocationConverter): Chain.Asset? {
val assetId = findChargeAssetTxPayment()?.assetId ?: return null
return multiLocationConverter.toChainAsset(assetId)
}
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_account_impl.data.fee.chains
import io.novafoundation.nova.feature_account_api.data.fee.DefaultFastLookupCustomFeeCapability
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.capability.FastLookupCustomFeeCapability
import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import kotlinx.coroutines.CoroutineScope
class DefaultFeePaymentProvider(override val chain: Chain) : FeePaymentProvider {
override suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment {
return NativeFeePayment()
}
override suspend fun detectFeePaymentFromExtrinsic(extrinsic: SendableExtrinsic): FeePayment {
return NativeFeePayment()
}
override suspend fun canPayFee(feePaymentCurrency: FeePaymentCurrency): Result<Boolean> {
val result = when (feePaymentCurrency) {
is FeePaymentCurrency.Asset -> false
FeePaymentCurrency.Native -> true
}
return Result.success(result)
}
override suspend fun fastLookupCustomFeeCapability(): Result<FastLookupCustomFeeCapability> {
return Result.success(DefaultFastLookupCustomFeeCapability())
}
}
@@ -0,0 +1,64 @@
package io.novafoundation.nova.feature_account_impl.data.fee.chains
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.novafoundation.nova.feature_account_api.data.fee.FeePayment
import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability
import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider
import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydraDxQuoteSharedComputation
import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydrationConversionFeePayment
import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydrationFastLookupFeeCapability
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationAcceptedFeeCurrenciesFetcher
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationPriceConversionFallback
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import kotlinx.coroutines.CoroutineScope
class HydrationFeePaymentProvider @AssistedInject constructor(
@Assisted override val chain: Chain,
private val chainRegistry: ChainRegistry,
private val hydraDxQuoteSharedComputation: HydraDxQuoteSharedComputation,
private val hydrationFeeInjector: HydrationFeeInjector,
private val hydrationPriceConversionFallback: HydrationPriceConversionFallback,
private val accountRepository: AccountRepository,
private val hydrationAcceptedFeeCurrenciesFetcher: HydrationAcceptedFeeCurrenciesFetcher
) : CustomOrNativeFeePaymentProvider() {
@AssistedFactory
interface Factory {
fun create(chain: Chain): HydrationFeePaymentProvider
}
override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment {
return HydrationConversionFeePayment(
paymentAsset = customFeeAsset,
chainRegistry = chainRegistry,
hydrationFeeInjector = hydrationFeeInjector,
hydraDxQuoteSharedComputation = hydraDxQuoteSharedComputation,
accountRepository = accountRepository,
coroutineScope = coroutineScope!!,
hydrationPriceConversionFallback = hydrationPriceConversionFallback,
hydrationAcceptedFeeCurrenciesFetcher = hydrationAcceptedFeeCurrenciesFetcher
)
}
override suspend fun canPayFeeInNonUtilityToken(customFeeAsset: Chain.Asset): Result<Boolean> {
return hydrationAcceptedFeeCurrenciesFetcher.isAcceptedCurrency(customFeeAsset)
}
override suspend fun detectFeePaymentFromExtrinsic(extrinsic: SendableExtrinsic): FeePayment {
// Todo Hydration fee support from extrinsic
return NativeFeePayment()
}
override suspend fun fastLookupCustomFeeCapability(): Result<FastLookupCustomFeeCapability> {
return hydrationAcceptedFeeCurrenciesFetcher.fetchAcceptedFeeCurrencies(chain)
.map(::HydrationFastLookupFeeCapability)
}
}
@@ -0,0 +1,79 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub
import android.util.Log
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.assetConversionAssetIdType
import io.novafoundation.nova.feature_account_api.data.fee.FeePayment
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.converter.toMultiLocationOrThrow
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Interior
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
import io.novafoundation.nova.feature_xcm_api.versions.orDefault
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
import io.novafoundation.nova.runtime.extrinsic.extensions.ChargeAssetTxPayment.Companion.chargeAssetTxPayment
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import java.math.BigInteger
internal class AssetConversionFeePayment(
private val paymentAsset: Chain.Asset,
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
private val multiLocationConverter: MultiLocationConverter,
private val xcmVersionDetector: XcmVersionDetector
) : FeePayment {
override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) {
val xcmVersion = detectAssetIdXcmVersion(extrinsicBuilder.runtime)
return extrinsicBuilder.chargeAssetTxPayment(encodableAssetId(xcmVersion))
}
override suspend fun convertNativeFee(nativeFee: Fee): Fee {
val quote = multiChainRuntimeCallsApi.forChain(paymentAsset.chainId).convertNativeFee(nativeFee.amount)
requireNotNull(quote) {
Log.e(LOG_TAG, "Quote for ${paymentAsset.symbol} fee was null")
"Failed to calculate fee in ${paymentAsset.symbol}"
}
return SubstrateFee(amount = quote, submissionOrigin = nativeFee.submissionOrigin, asset = paymentAsset)
}
private suspend fun encodableAssetId(xcmVersion: XcmVersion): Any {
return multiLocationConverter.toMultiLocationOrThrow(paymentAsset).toEncodableInstance(xcmVersion)
}
private fun encodableNativeAssetId(xcmVersion: XcmVersion): Any {
return RelativeMultiLocation(
parents = 1,
interior = Interior.Here
).toEncodableInstance(xcmVersion)
}
private suspend fun RuntimeCallsApi.convertNativeFee(amount: BigInteger): BigInteger? {
val xcmVersion = detectAssetIdXcmVersion(runtime)
return call(
section = "AssetConversionApi",
method = "quote_price_tokens_for_exact_tokens",
arguments = mapOf(
"asset1" to encodableAssetId(xcmVersion),
"asset2" to encodableNativeAssetId(xcmVersion),
"amount" to amount,
"include_fee" to true
),
returnBinding = ::bindNumberOrNull
)
}
private suspend fun detectAssetIdXcmVersion(runtime: RuntimeSnapshot): XcmVersion {
val assetIdType = runtime.metadata.assetConversionAssetIdType()
return xcmVersionDetector.detectMultiLocationVersion(paymentAsset.chainId, assetIdType).orDefault()
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub
import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability
class AssetHubFastLookupFeeCapability(
override val nonUtilityFeeCapableTokens: Set<Int>,
) : FastLookupCustomFeeCapability
@@ -0,0 +1,60 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub
import io.novafoundation.nova.common.utils.mapNotNullToSet
import io.novafoundation.nova.feature_account_api.data.conversion.assethub.assetConversionOrNull
import io.novafoundation.nova.feature_account_api.data.conversion.assethub.pools
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.runtime.ext.isUtilityAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.common.utils.metadata
interface AssetHubFeePaymentAssetsFetcher {
suspend fun fetchAvailablePaymentAssets(): Set<ChainAssetId>
}
class AssetHubFeePaymentAssetsFetcherFactory(
private val remoteStorageSource: StorageDataSource,
private val multiLocationConverterFactory: MultiLocationConverterFactory
) {
suspend fun create(chain: Chain): AssetHubFeePaymentAssetsFetcher {
val multiLocationConverter = multiLocationConverterFactory.defaultSync(chain)
return RealAssetHubFeePaymentAssetsFetcher(remoteStorageSource, multiLocationConverter, chain)
}
fun create(chain: Chain, multiLocationConverter: MultiLocationConverter): AssetHubFeePaymentAssetsFetcher {
return RealAssetHubFeePaymentAssetsFetcher(remoteStorageSource, multiLocationConverter, chain)
}
}
private class RealAssetHubFeePaymentAssetsFetcher(
private val remoteStorageSource: StorageDataSource,
private val multiLocationConverter: MultiLocationConverter,
private val chain: Chain,
) : AssetHubFeePaymentAssetsFetcher {
override suspend fun fetchAvailablePaymentAssets(): Set<ChainAssetId> {
return remoteStorageSource.query(chain.id) {
val allPools = metadata.assetConversionOrNull?.pools?.keys().orEmpty()
constructAvailableCustomFeeAssets(allPools)
}
}
private suspend fun constructAvailableCustomFeeAssets(pools: List<Pair<RelativeMultiLocation, RelativeMultiLocation>>): Set<Int> {
return pools.mapNotNullToSet { (firstLocation, secondLocation) ->
val firstAsset = multiLocationConverter.toChainAsset(firstLocation) ?: return@mapNotNullToSet null
if (!firstAsset.isUtilityAsset) return@mapNotNullToSet null
val secondAsset = multiLocationConverter.toChainAsset(secondLocation) ?: return@mapNotNullToSet null
secondAsset.id
}
}
}
@@ -0,0 +1,88 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra
import io.novafoundation.nova.common.data.memory.ComputationalCache
import io.novafoundation.nova.common.data.memory.SharedComputation
import io.novafoundation.nova.common.data.memory.SharedFlowCache
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.utils.graph.Graph
import io.novafoundation.nova.common.utils.graph.create
import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter
import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting
import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuotingSubscriptions
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
class HydraDxQuoteSharedComputation(
private val computationalCache: ComputationalCache,
private val quotingFactory: HydraDxQuoting.Factory,
private val pathQuoterFactory: PathQuoter.Factory,
private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
private val chainStateRepository: ChainStateRepository,
) : SharedComputation(computationalCache) {
suspend fun getQuoter(
chain: Chain,
accountId: AccountId,
scope: CoroutineScope
): PathQuoter<QuotableEdge> {
val key = "HydraDxQuoter:${chain.id}:${accountId.toHexString()}"
return computationalCache.useCache(key, scope) {
val assetConversion = getSwapQuoting(chain, accountId, scope)
val edges = assetConversion.availableSwapDirections()
val graph = Graph.create(edges)
pathQuoterFactory.create(flowOf(graph), scope)
}
}
suspend fun getSwapQuoting(
chain: Chain,
accountId: AccountId,
scope: CoroutineScope
): SwapQuoting {
val key = "HydraDxAssetConversion:${chain.id}:${accountId.toHexString()}"
return computationalCache.useCache(key, scope) {
val sharedSubscriptions = RealSwapQuotingSubscriptions(scope)
val host = RealQuotingHost(sharedSubscriptions)
val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id)
val hydraDxQuoting = quotingFactory.create(chain, host)
hydraDxQuoting.sync()
hydraDxQuoting.runSubscriptions(accountId, subscriptionBuilder)
.launchIn(this)
subscriptionBuilder.subscribe(this)
hydraDxQuoting
}
}
private class RealQuotingHost(
override val sharedSubscriptions: SwapQuotingSubscriptions,
) : SwapQuoting.QuotingHost
private inner class RealSwapQuotingSubscriptions(scope: CoroutineScope) : SwapQuotingSubscriptions {
private val blockNumberCache = SharedFlowCache<ChainId, BlockNumber>(scope) { chainId ->
chainStateRepository.currentRemoteBlockNumberFlow(chainId)
}
override suspend fun blockNumber(chainId: ChainId): Flow<BlockNumber> {
return blockNumberCache.getOrCompute(chainId)
}
}
}
@@ -0,0 +1,66 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra
import io.novafoundation.nova.feature_account_api.data.fee.FeePayment
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.ResetMode
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.SetFeesMode
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.SetMode
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.feature_swap_core_api.data.paths.model.quote
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationAcceptedFeeCurrenciesFetcher
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationPriceConversionFallback
import io.novafoundation.nova.runtime.ext.commissionAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import kotlinx.coroutines.CoroutineScope
internal class HydrationConversionFeePayment(
private val paymentAsset: Chain.Asset,
private val chainRegistry: ChainRegistry,
private val hydrationFeeInjector: HydrationFeeInjector,
private val hydraDxQuoteSharedComputation: HydraDxQuoteSharedComputation,
private val hydrationPriceConversionFallback: HydrationPriceConversionFallback,
private val hydrationAcceptedFeeCurrenciesFetcher: HydrationAcceptedFeeCurrenciesFetcher,
private val accountRepository: AccountRepository,
private val coroutineScope: CoroutineScope
) : FeePayment {
override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) {
val setFeesMode = SetFeesMode(
setMode = SetMode.Always,
resetMode = ResetMode.ToNative
)
hydrationFeeInjector.setFees(extrinsicBuilder, paymentAsset, setFeesMode)
}
override suspend fun convertNativeFee(nativeFee: Fee): Fee {
val metaAccount = accountRepository.getSelectedMetaAccount()
val chain = chainRegistry.getChain(paymentAsset.chainId)
val accountId = metaAccount.requireAccountIdIn(chain)
val fromAsset = chain.commissionAsset
val quoter = hydraDxQuoteSharedComputation.getQuoter(chain, accountId, coroutineScope)
val convertedAmount = runCatching {
quoter.findBestPath(
chainAssetIn = fromAsset,
chainAssetOut = paymentAsset,
amount = nativeFee.amount,
swapDirection = SwapDirection.SPECIFIED_IN
).bestPath.quote
}
.recoverCatching { hydrationPriceConversionFallback.convertNativeAmount(nativeFee.amount, paymentAsset) }
.getOrThrow()
return SubstrateFee(
amount = convertedAmount,
submissionOrigin = nativeFee.submissionOrigin,
asset = paymentAsset
)
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra
import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
class HydrationFastLookupFeeCapability(
override val nonUtilityFeeCapableTokens: Set<ChainAssetId>
) : FastLookupCustomFeeCapability
@@ -0,0 +1,122 @@
package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter
import io.novafoundation.nova.feature_swap_core_api.data.network.toOnChainIdOrThrow
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
internal class RealHydrationFeeInjector(
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
) : HydrationFeeInjector {
override suspend fun setFees(
extrinsicBuilder: ExtrinsicBuilder,
paymentAsset: Chain.Asset,
mode: HydrationFeeInjector.SetFeesMode
) {
val baseCalls = extrinsicBuilder.getCalls()
extrinsicBuilder.resetCalls()
val justSetFees = getSetPhase(mode.setMode).setFees(extrinsicBuilder, paymentAsset)
extrinsicBuilder.addCalls(baseCalls)
getResetPhase(mode.resetMode).resetFees(extrinsicBuilder, justSetFees)
}
private fun getSetPhase(mode: HydrationFeeInjector.SetMode): SetPhase {
return when (mode) {
HydrationFeeInjector.SetMode.Always -> AlwaysSetPhase()
is HydrationFeeInjector.SetMode.Lazy -> LazySetPhase(mode.currentlySetFeeAsset)
}
}
private fun getResetPhase(mode: HydrationFeeInjector.ResetMode): ResetPhase {
return when (mode) {
HydrationFeeInjector.ResetMode.ToNative -> AlwaysResetPhase()
is HydrationFeeInjector.ResetMode.ToNativeLazily -> LazyResetPhase(mode.feeAssetBeforeTransaction)
}
}
private interface SetPhase {
/**
* @return just set on-chain asset id, if changed
*/
suspend fun setFees(extrinsicBuilder: ExtrinsicBuilder, paymentAsset: Chain.Asset): HydraDxAssetId?
}
private interface ResetPhase {
suspend fun resetFees(
extrinsicBuilder: ExtrinsicBuilder,
feesModifiedInSetPhase: HydraDxAssetId?
)
}
private inner class AlwaysSetPhase : SetPhase {
override suspend fun setFees(extrinsicBuilder: ExtrinsicBuilder, paymentAsset: Chain.Asset): HydraDxAssetId {
val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(paymentAsset)
extrinsicBuilder.setFeeCurrency(onChainId)
return onChainId
}
}
private inner class LazySetPhase(
private val currentFeeTokenId: HydraDxAssetId,
) : SetPhase {
override suspend fun setFees(extrinsicBuilder: ExtrinsicBuilder, paymentAsset: Chain.Asset): HydraDxAssetId? {
val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(paymentAsset)
paymentCurrencyToSet?.let {
extrinsicBuilder.setFeeCurrency(paymentCurrencyToSet)
}
return paymentCurrencyToSet
}
private suspend fun getPaymentCurrencyToSetIfNeeded(expectedPaymentAsset: Chain.Asset): HydraDxAssetId? {
val expectedPaymentTokenId = hydraDxAssetIdConverter.toOnChainIdOrThrow(expectedPaymentAsset)
return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId }
}
}
private inner class AlwaysResetPhase : ResetPhase {
override suspend fun resetFees(
extrinsicBuilder: ExtrinsicBuilder,
feesModifiedInSetPhase: HydraDxAssetId?
) {
extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.systemAssetId)
}
}
private inner class LazyResetPhase(
private val previousFeeCurrency: HydraDxAssetId
) : ResetPhase {
override suspend fun resetFees(extrinsicBuilder: ExtrinsicBuilder, feesModifiedInSetPhase: HydraDxAssetId?) {
val justSetFeeToNonNative = feesModifiedInSetPhase != null && feesModifiedInSetPhase != hydraDxAssetIdConverter.systemAssetId
val previousCurrencyRemainsNonNative = feesModifiedInSetPhase == null && previousFeeCurrency != hydraDxAssetIdConverter.systemAssetId
if (justSetFeeToNonNative || previousCurrencyRemainsNonNative) {
extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.systemAssetId)
}
}
}
private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) {
call(
moduleName = Modules.MULTI_TRANSACTION_PAYMENT,
callName = "set_currency",
arguments = mapOf(
"currency" to onChainId
)
)
}
}
@@ -0,0 +1,207 @@
package io.novafoundation.nova.feature_account_impl.data.mappers
import android.util.Log
import com.google.gson.Gson
import io.novafoundation.nova.common.utils.filterNotNull
import io.novafoundation.nova.common.utils.fromJson
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.JoinedMetaAccountInfo
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MultisigTypeExtras
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_impl.data.multisig.MultisigRepository
import io.novafoundation.nova.feature_account_impl.domain.account.model.DefaultMetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.GenericLedgerMetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.LegacyLedgerMetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.PolkadotVaultMetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.RealMultisigMetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.RealProxiedMetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.RealSecretsMetaAccount
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class AccountMappers(
private val ledgerMigrationTracker: LedgerMigrationTracker,
private val gson: Gson,
private val multisigRepository: MultisigRepository
) {
suspend fun mapMetaAccountsLocalToMetaAccounts(joinedMetaAccountInfo: List<JoinedMetaAccountInfo>): List<MetaAccount> {
val supportedGenericLedgerChains = ledgerMigrationTracker.supportedChainIdsByGenericApp()
return joinedMetaAccountInfo.mapNotNull {
mapMetaAccountLocalToMetaAccount(it) { supportedGenericLedgerChains }
}
}
suspend fun mapMetaAccountLocalToMetaAccount(joinedMetaAccountInfo: JoinedMetaAccountInfo): MetaAccount {
return mapMetaAccountLocalToMetaAccount(joinedMetaAccountInfo) {
ledgerMigrationTracker.supportedChainIdsByGenericApp()
}!!
}
private suspend fun mapMetaAccountLocalToMetaAccount(
joinedMetaAccountInfo: JoinedMetaAccountInfo,
supportedGenericLedgerChains: suspend () -> Set<ChainId>
): MetaAccount? {
val chainAccounts = joinedMetaAccountInfo.chainAccounts.associateBy(
keySelector = ChainAccountLocal::chainId,
valueTransform = {
mapChainAccountFromLocal(it)
}
).filterNotNull()
return with(joinedMetaAccountInfo.metaAccount) {
when (val type = mapMetaAccountTypeFromLocal(type)) {
LightMetaAccount.Type.SECRETS -> RealSecretsMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
chainAccounts = chainAccounts,
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
isSelected = isSelected,
name = name,
status = mapMetaAccountStateFromLocal(status),
parentMetaId = parentMetaId
)
LightMetaAccount.Type.WATCH_ONLY -> DefaultMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
chainAccounts = chainAccounts,
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
isSelected = isSelected,
name = name,
type = type,
status = mapMetaAccountStateFromLocal(status),
parentMetaId = parentMetaId
)
LightMetaAccount.Type.PARITY_SIGNER,
LightMetaAccount.Type.POLKADOT_VAULT -> PolkadotVaultMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
chainAccounts = chainAccounts,
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
isSelected = isSelected,
name = name,
type = type,
status = mapMetaAccountStateFromLocal(status),
parentMetaId = parentMetaId
)
LightMetaAccount.Type.LEDGER -> GenericLedgerMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
chainAccounts = chainAccounts,
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
isSelected = isSelected,
name = name,
type = type,
status = mapMetaAccountStateFromLocal(status),
supportedGenericLedgerChains = supportedGenericLedgerChains(),
parentMetaId = parentMetaId
)
LightMetaAccount.Type.LEDGER_LEGACY -> LegacyLedgerMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
chainAccounts = chainAccounts,
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
isSelected = isSelected,
name = name,
type = type,
status = mapMetaAccountStateFromLocal(status),
parentMetaId = parentMetaId
)
LightMetaAccount.Type.PROXIED -> {
val proxyAccount = joinedMetaAccountInfo.proxyAccountLocal?.let {
mapProxyAccountFromLocal(it)
}
RealProxiedMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
chainAccounts = chainAccounts,
proxy = proxyAccount ?: run {
Log.e("Proxy", "Null proxy account for proxied $id ($name)")
return null
},
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
isSelected = isSelected,
name = name,
status = mapMetaAccountStateFromLocal(status),
parentMetaId = parentMetaId
)
}
LightMetaAccount.Type.MULTISIG -> {
val multisigTypeExtras = gson.fromJson<MultisigTypeExtras>(requireNotNull(typeExtras) { "typeExtras is null: $id" })
RealMultisigMetaAccount(
id = id,
globallyUniqueId = globallyUniqueId,
substrateAccountId = substrateAccountId,
ethereumAddress = ethereumAddress,
ethereumPublicKey = ethereumPublicKey,
chainAccounts = chainAccounts,
isSelected = isSelected,
name = name,
status = mapMetaAccountStateFromLocal(status),
signatoryMetaId = requireNotNull(parentMetaId) { "parentMetaId is null: $id" },
otherSignatoriesUnsorted = multisigTypeExtras.otherSignatories,
threshold = multisigTypeExtras.threshold,
signatoryAccountId = multisigTypeExtras.signatoryAccountId,
parentMetaId = parentMetaId,
multisigRepository = multisigRepository
)
}
}
}
}
fun mapMetaAccountTypeFromLocal(local: MetaAccountLocal.Type): LightMetaAccount.Type {
return when (local) {
MetaAccountLocal.Type.SECRETS -> LightMetaAccount.Type.SECRETS
MetaAccountLocal.Type.WATCH_ONLY -> LightMetaAccount.Type.WATCH_ONLY
MetaAccountLocal.Type.PARITY_SIGNER -> LightMetaAccount.Type.PARITY_SIGNER
MetaAccountLocal.Type.LEDGER -> LightMetaAccount.Type.LEDGER_LEGACY
MetaAccountLocal.Type.LEDGER_GENERIC -> LightMetaAccount.Type.LEDGER
MetaAccountLocal.Type.POLKADOT_VAULT -> LightMetaAccount.Type.POLKADOT_VAULT
MetaAccountLocal.Type.PROXIED -> LightMetaAccount.Type.PROXIED
MetaAccountLocal.Type.MULTISIG -> LightMetaAccount.Type.MULTISIG
}
}
private fun mapMetaAccountStateFromLocal(local: MetaAccountLocal.Status): LightMetaAccount.Status {
return when (local) {
MetaAccountLocal.Status.ACTIVE -> LightMetaAccount.Status.ACTIVE
MetaAccountLocal.Status.DEACTIVATED -> LightMetaAccount.Status.DEACTIVATED
}
}
}
@@ -0,0 +1,168 @@
package io.novafoundation.nova.feature_account_impl.data.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.asPrecision
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core.model.Node
import io.novafoundation.nova.core.model.Node.NetworkType
import io.novafoundation.nova.core_db.dao.MetaAccountWithBalanceLocal
import io.novafoundation.nova.core_db.model.NodeLocal
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.ProxyAccountLocal
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
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.ProxyAccount
import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload
import io.novafoundation.nova.feature_account_impl.R
import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.api.AccountNameChooserMixin
import io.novafoundation.nova.feature_account_impl.presentation.node.model.NodeModel
import io.novafoundation.nova.feature_account_impl.presentation.view.advanced.encryption.model.CryptoTypeModel
import io.novafoundation.nova.feature_account_impl.presentation.view.advanced.network.model.NetworkModel
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novafoundation.nova.feature_proxy_api.domain.model.fromString
fun mapNetworkTypeToNetworkModel(networkType: NetworkType): NetworkModel {
val type = when (networkType) {
NetworkType.KUSAMA -> NetworkModel.NetworkTypeUI.Kusama
NetworkType.POLKADOT -> NetworkModel.NetworkTypeUI.Polkadot
NetworkType.WESTEND -> NetworkModel.NetworkTypeUI.Westend
NetworkType.ROCOCO -> NetworkModel.NetworkTypeUI.Rococo
}
return NetworkModel(networkType.readableName, type)
}
fun mapCryptoTypeToCryptoTypeModel(
resourceManager: ResourceManager,
encryptionType: CryptoType
): CryptoTypeModel {
val title = mapCryptoTypeToCryptoTypeTitle(resourceManager, encryptionType)
val subtitle = mapCryptoTypeToCryptoTypeSubtitle(resourceManager, encryptionType)
return CryptoTypeModel("$title $subtitle", encryptionType)
}
fun mapCryptoTypeToCryptoTypeTitle(
resourceManager: ResourceManager,
encryptionType: CryptoType
): String {
return when (encryptionType) {
CryptoType.SR25519 -> resourceManager.getString(R.string.sr25519_selection_title)
CryptoType.ED25519 -> resourceManager.getString(R.string.ed25519_selection_title)
CryptoType.ECDSA -> resourceManager.getString(R.string.ecdsa_selection_title)
}
}
fun mapCryptoTypeToCryptoTypeSubtitle(
resourceManager: ResourceManager,
encryptionType: CryptoType
): String {
return when (encryptionType) {
CryptoType.SR25519 -> resourceManager.getString(R.string.sr25519_selection_subtitle)
CryptoType.ED25519 -> resourceManager.getString(R.string.ed25519_selection_subtitle)
CryptoType.ECDSA -> resourceManager.getString(R.string.ecdsa_selection_subtitle)
}
}
fun mapNodeToNodeModel(node: Node): NodeModel {
val networkModelType = mapNetworkTypeToNetworkModel(node.networkType)
return with(node) {
NodeModel(
id = id,
name = name,
link = link,
networkModelType = networkModelType.networkTypeUI,
isDefault = isDefault,
isActive = isActive
)
}
}
fun mapNodeLocalToNode(nodeLocal: NodeLocal): Node {
return with(nodeLocal) {
Node(
id = id,
name = name,
networkType = NetworkType.values()[nodeLocal.networkType],
link = link,
isActive = isActive,
isDefault = isDefault
)
}
}
fun mapMetaAccountTypeToLocal(local: LightMetaAccount.Type): MetaAccountLocal.Type {
return when (local) {
LightMetaAccount.Type.SECRETS -> MetaAccountLocal.Type.SECRETS
LightMetaAccount.Type.WATCH_ONLY -> MetaAccountLocal.Type.WATCH_ONLY
LightMetaAccount.Type.PARITY_SIGNER -> MetaAccountLocal.Type.PARITY_SIGNER
LightMetaAccount.Type.LEDGER_LEGACY -> MetaAccountLocal.Type.LEDGER
LightMetaAccount.Type.LEDGER -> MetaAccountLocal.Type.LEDGER_GENERIC
LightMetaAccount.Type.POLKADOT_VAULT -> MetaAccountLocal.Type.POLKADOT_VAULT
LightMetaAccount.Type.PROXIED -> MetaAccountLocal.Type.PROXIED
LightMetaAccount.Type.MULTISIG -> MetaAccountLocal.Type.MULTISIG
}
}
fun mapMetaAccountWithBalanceFromLocal(local: MetaAccountWithBalanceLocal): MetaAccountAssetBalance {
return with(local) {
MetaAccountAssetBalance(
metaId = id,
freeInPlanks = freeInPlanks,
reservedInPlanks = reservedInPlanks,
offChainBalance = offChainBalance,
precision = precision.asPrecision(),
rate = rate,
)
}
}
fun mapChainAccountFromLocal(chainAccountLocal: ChainAccountLocal): MetaAccount.ChainAccount {
return with(chainAccountLocal) {
MetaAccount.ChainAccount(
metaId = metaId,
publicKey = publicKey,
chainId = chainId,
accountId = accountId,
cryptoType = cryptoType
)
}
}
fun mapProxyAccountFromLocal(proxyAccountLocal: ProxyAccountLocal): ProxyAccount {
return with(proxyAccountLocal) {
ProxyAccount(
proxyMetaId = proxyMetaId,
chainId = chainId,
proxyType = ProxyType.fromString(proxyType)
)
}
}
fun mapAddAccountPayloadToAddAccountType(
payload: AddAccountPayload,
accountNameState: AccountNameChooserMixin.State,
): AddAccountType {
return when (payload) {
AddAccountPayload.MetaAccount -> {
require(accountNameState is AccountNameChooserMixin.State.Input) { "Name input should be present for meta account" }
AddAccountType.MetaAccount(accountNameState.value)
}
is AddAccountPayload.ChainAccount -> AddAccountType.ChainAccount(payload.chainId, payload.metaId)
}
}
fun mapOptionalNameToNameChooserState(name: String?) = when (name) {
null -> AccountNameChooserMixin.State.NoInput
else -> AccountNameChooserMixin.State.Input(name)
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_impl.data.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.model.OnChainMultisig
import io.novafoundation.nova.feature_account_impl.data.multisig.model.DiscoveredMultisig
import io.novafoundation.nova.feature_account_impl.data.multisig.model.OffChainPendingMultisigOperationInfo
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
interface MultisigRepository {
fun supportsMultisigSync(chain: Chain): Boolean
suspend fun findMultisigAccounts(accountIds: Set<AccountIdKey>): List<DiscoveredMultisig>
suspend fun getPendingOperationIds(chain: Chain, accountIdKey: AccountIdKey): Set<CallHash>
suspend fun subscribePendingOperations(
chain: Chain,
accountIdKey: AccountIdKey,
operationIds: Collection<CallHash>
): Flow<Map<CallHash, OnChainMultisig?>>
suspend fun getOffChainPendingOperationsInfo(
chain: Chain,
accountId: AccountIdKey,
pendingCallHashes: Collection<CallHash>
): Result<Map<CallHash, OffChainPendingMultisigOperationInfo>>
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_impl.data.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigDetailsRepository
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.model.OnChainMultisig
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.multisig
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.multisigs
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Inject
import javax.inject.Named
@FeatureScope
class RealMultisigDetailsRepository @Inject constructor(
@Named(REMOTE_STORAGE_SOURCE) private val remoteStorageSource: StorageDataSource
) : MultisigDetailsRepository {
override suspend fun hasMultisigOperation(chain: Chain, accountIdKey: AccountIdKey, callHash: CallHash): Boolean {
return getOnChainMultisig(chain, accountIdKey, callHash) != null
}
private suspend fun getOnChainMultisig(chain: Chain, accountIdKey: AccountIdKey, operationId: CallHash): OnChainMultisig? {
return remoteStorageSource.query(chain.id) {
runtime.metadata.multisig.multisigs.query(accountIdKey, operationId)
}
}
}
@@ -0,0 +1,155 @@
package io.novafoundation.nova.feature_account_impl.data.multisig
import android.util.Log
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.fromHexOrNull
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.config.GlobalConfigDataSource
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.HexString
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.callHash
import io.novafoundation.nova.common.utils.fromHex
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.feature_account_impl.data.multisig.api.FindMultisigsApi
import io.novafoundation.nova.feature_account_impl.data.multisig.api.request.FindMultisigsRequest
import io.novafoundation.nova.feature_account_impl.data.multisig.api.request.OffChainPendingMultisigInfoRequest
import io.novafoundation.nova.feature_account_impl.data.multisig.api.response.AccountMultisigRemote
import io.novafoundation.nova.feature_account_impl.data.multisig.api.response.FindMultisigsResponse
import io.novafoundation.nova.feature_account_impl.data.multisig.api.response.GetPedingMultisigOperationsResponse
import io.novafoundation.nova.feature_account_impl.data.multisig.api.response.GetPedingMultisigOperationsResponse.OperationRemote
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.model.OnChainMultisig
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.multisig
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.multisigs
import io.novafoundation.nova.feature_account_impl.data.multisig.model.DiscoveredMultisig
import io.novafoundation.nova.feature_account_impl.data.multisig.model.OffChainPendingMultisigOperationInfo
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Named
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@FeatureScope
class RealMultisigRepository @Inject constructor(
private val api: FindMultisigsApi,
@Named(REMOTE_STORAGE_SOURCE)
private val remoteStorageSource: StorageDataSource,
private val chainRegistry: ChainRegistry,
private val globalConfigDataSource: GlobalConfigDataSource
) : MultisigRepository {
override fun supportsMultisigSync(chain: Chain): Boolean {
return chain.multisigSupport
}
override suspend fun findMultisigAccounts(accountIds: Set<AccountIdKey>): List<DiscoveredMultisig> {
val globalConfig = globalConfigDataSource.getGlobalConfig()
val request = FindMultisigsRequest(accountIds)
return api.findMultisigs(globalConfig.multisigsApiUrl, request).toDiscoveredMultisigs()
}
override suspend fun getPendingOperationIds(chain: Chain, accountIdKey: AccountIdKey): Set<CallHash> {
return remoteStorageSource.query(chain.id) {
runtime.metadata.multisig.multisigs.keys(accountIdKey)
.mapToSet { it.second }
}
}
override suspend fun subscribePendingOperations(
chain: Chain,
accountIdKey: AccountIdKey,
operationIds: Collection<CallHash>
): Flow<Map<CallHash, OnChainMultisig?>> {
return remoteStorageSource.subscribeBatched(chain.id) {
val allKeys = operationIds.map { accountIdKey to it }
runtime.metadata.multisig.multisigs.observe(allKeys).map { operationsByKeys ->
operationsByKeys.mapKeys { (key, _) -> key.second }
}
}
}
override suspend fun getOffChainPendingOperationsInfo(
chain: Chain,
accountId: AccountIdKey,
pendingCallHashes: Collection<CallHash>
): Result<Map<CallHash, OffChainPendingMultisigOperationInfo>> {
return runCatching {
val globalConfig = globalConfigDataSource.getGlobalConfig()
val request = OffChainPendingMultisigInfoRequest(accountId, pendingCallHashes, chain.id)
val response = api.getCallDatas(globalConfig.multisigsApiUrl, request)
response.toDomain(chain)
}
.onFailure { Log.e("RealMultisigRepository", "Failed to fetch call datas in ${chain.name}", it) }
}
private suspend fun SubQueryResponse<GetPedingMultisigOperationsResponse>.toDomain(chain: Chain): Map<CallHash, OffChainPendingMultisigOperationInfo> {
return chainRegistry.withRuntime(chain.id) {
data.multisigOperations.nodes.mapNotNull { multisigOperation ->
val callHash = CallHash.fromHexOrNull(multisigOperation.callHash) ?: return@mapNotNull null
val callData = parseCallData(multisigOperation.callData, callHash, chain)
OffChainPendingMultisigOperationInfo(
timestamp = multisigOperation.timestamp(),
callData = callData,
callHash = callHash
)
}
.associateBy { it.callHash }
}
}
private fun OperationRemote.timestamp(): Duration {
val inSeconds = events.nodes.firstOrNull()?.timestamp ?: timestamp
return inSeconds.seconds
}
context(RuntimeContext)
private fun parseCallData(
callData: HexString?,
callHash: CallHash,
chain: Chain
): GenericCall.Instance? {
if (callData == null) return null
return runCatching {
val hashFromCallData = callData.callHash().intoKey()
require(hashFromCallData == callHash) {
"Call-data does not match call hash. Expected hash: $callHash, Actual hash: $hashFromCallData. Call data: $callData"
}
GenericCall.fromHex(callData)
}
.onFailure { Log.e("RealMultisigRepository", "Failed to decode call data on ${chain.name}: $callData}", it) }
.getOrNull()
}
private fun SubQueryResponse<FindMultisigsResponse>.toDiscoveredMultisigs(): List<DiscoveredMultisig> {
return data.accountMultisigs.nodes.mapNotNull { multisigNode ->
val multisig = multisigNode.multisig
DiscoveredMultisig(
accountId = AccountIdKey.fromHexOrNull(multisig.accountId) ?: return@mapNotNull null,
threshold = multisig.thresholdIfValid() ?: return@mapNotNull null,
allSignatories = multisig.signatories.nodes.map { signatoryNode ->
AccountIdKey.fromHexOrNull(signatoryNode.signatoryId) ?: return@mapNotNull null
}
)
}
}
private fun AccountMultisigRemote.MultisigRemote.thresholdIfValid(): Int? {
// Just to be sure we do not insert some invalid data
return threshold.takeIf { it >= 1 }
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.api
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
import io.novafoundation.nova.feature_account_impl.data.multisig.api.request.FindMultisigsRequest
import io.novafoundation.nova.feature_account_impl.data.multisig.api.request.OffChainPendingMultisigInfoRequest
import io.novafoundation.nova.feature_account_impl.data.multisig.api.response.FindMultisigsResponse
import io.novafoundation.nova.feature_account_impl.data.multisig.api.response.GetPedingMultisigOperationsResponse
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
interface FindMultisigsApi {
@POST
suspend fun findMultisigs(
@Url url: String,
@Body body: FindMultisigsRequest
): SubQueryResponse<FindMultisigsResponse>
@POST
suspend fun getCallDatas(
@Url url: String,
@Body body: OffChainPendingMultisigInfoRequest
): SubQueryResponse<GetPedingMultisigOperationsResponse>
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.api.request
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.toHexWithPrefix
import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters
class FindMultisigsRequest(
accountIds: Set<AccountIdKey>
) : SubQueryFilters {
@Transient
private val accountIdsHex = accountIds.map { it.toHexWithPrefix() }
val query = """
query {
accountMultisigs(
filter: {
signatory: {
${"id" presentIn accountIdsHex}
}
}
) {
nodes {
multisig {
threshold
signatories {
nodes {
signatoryId
}
}
accountId
}
}
}
}
""".trimIndent()
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.api.request
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.toHexWithPrefix
import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
class OffChainPendingMultisigInfoRequest(
accountIdKey: AccountIdKey,
callHashes: Collection<CallHash>,
chainId: ChainId
) : SubQueryFilters {
@Transient
private val callHashesHex = callHashes.map { it.toHexWithPrefix() }
val query = """
query {
multisigOperations(filter: {
${"accountId" equalTo accountIdKey.toHexWithPrefix() }
${"status" equalToEnum "pending"}
${"callHash" presentIn callHashesHex}
${"chainId" equalTo chainId.requireHexPrefix()}
}) {
nodes {
callHash
callData
timestamp
events(last: 1) {
nodes {
timestamp
}
}
}
}
}
""".trimIndent()
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.api.response
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
import io.novafoundation.nova.common.utils.HexString
class FindMultisigsResponse(
val accountMultisigs: SubQueryNodes<AccountMultisigRemote>
)
class AccountMultisigRemote(
val multisig: MultisigRemote,
) {
class MultisigRemote(
val accountId: HexString,
val threshold: Int,
val signatories: SubQueryNodes<SignatoryRemote>
)
class SignatoryRemote(
val signatoryId: HexString
)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.api.response
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
import io.novafoundation.nova.common.utils.HexString
class GetPedingMultisigOperationsResponse(val multisigOperations: SubQueryNodes<OperationRemote>) {
class OperationRemote(val callHash: HexString, val callData: HexString?, val timestamp: Long, val events: SubQueryNodes<OperationEvent>)
class OperationEvent(val timestamp: Long)
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.blockhain
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.multisig
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.model.OnChainMultisig
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.QueryableStorageEntry2
import io.novafoundation.nova.runtime.storage.source.query.api.converters.scaleDecoder
import io.novafoundation.nova.runtime.storage.source.query.api.converters.scaleEncoder
import io.novafoundation.nova.runtime.storage.source.query.api.storage2
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
@JvmInline
value class MultisigRuntimeApi(override val module: Module) : QueryableModule
context(StorageQueryContext)
val RuntimeMetadata.multisig: MultisigRuntimeApi
get() = MultisigRuntimeApi(multisig())
context(StorageQueryContext)
val MultisigRuntimeApi.multisigs: QueryableStorageEntry2<AccountIdKey, CallHash, OnChainMultisig>
get() = storage2(
name = "Multisigs",
binding = { decoded, _, callHash -> OnChainMultisig.bind(decoded, callHash) },
key1ToInternalConverter = AccountIdKey.scaleEncoder,
key1FromInternalConverter = AccountIdKey.scaleDecoder,
key2ToInternalConverter = CallHash.scaleEncoder,
key2FromInternalConverter = CallHash.scaleDecoder
)
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigTimePoint
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import java.math.BigInteger
class OnChainMultisig(
val callHash: CallHash,
val approvals: List<AccountIdKey>,
val deposit: BigInteger,
val depositor: AccountIdKey,
val timePoint: MultisigTimePoint,
) {
companion object {
fun bind(decoded: Any?, callHash: CallHash): OnChainMultisig {
val struct = decoded.castToStruct()
return OnChainMultisig(
callHash = callHash,
approvals = bindList(struct["approvals"], ::bindAccountIdKey),
deposit = bindNumber(struct["deposit"]),
depositor = bindAccountIdKey(struct["depositor"]),
timePoint = MultisigTimePoint.bind(struct["when"])
)
}
}
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.model
import io.novafoundation.nova.common.address.AccountIdKey
class DiscoveredMultisig(
val accountId: AccountIdKey,
val allSignatories: List<AccountIdKey>,
val threshold: Int,
)
fun DiscoveredMultisig.otherSignatories(signatory: AccountIdKey): List<AccountIdKey> {
return allSignatories - signatory
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.model
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import kotlin.time.Duration
class OffChainPendingMultisigOperationInfo(
val timestamp: Duration,
val callHash: CallHash,
val callData: GenericCall.Instance?
)
@@ -0,0 +1,48 @@
package io.novafoundation.nova.feature_account_impl.data.multisig.repository
import io.novafoundation.nova.common.utils.mapList
import io.novafoundation.nova.core_db.dao.MultisigOperationsDao
import io.novafoundation.nova.core_db.model.MultisigOperationCallLocal
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigOperationLocalCallRepository
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 io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.extensions.toHexString
import kotlinx.coroutines.flow.Flow
class RealMultisigOperationLocalCallRepository(
private val multisigOperationsDao: MultisigOperationsDao
) : MultisigOperationLocalCallRepository {
override suspend fun setMultisigCall(operation: SavedMultisigOperationCall) {
multisigOperationsDao.insertOperation(
MultisigOperationCallLocal(
chainId = operation.chainId,
metaId = operation.metaId,
callHash = operation.callHash.toHexString(),
callInstance = operation.callInstance
)
)
}
override fun callsFlow(): Flow<List<SavedMultisigOperationCall>> {
return multisigOperationsDao.observeOperations()
.mapList {
SavedMultisigOperationCall(
metaId = it.metaId,
chainId = it.chainId,
callHash = it.callHash.fromHex(),
callInstance = it.callInstance,
)
}
}
override suspend fun removeCallHashesExclude(metaId: Long, chainId: ChainId, excludedCallHashes: Set<CallHash>) {
multisigOperationsDao.removeOperationsExclude(
metaId,
chainId,
excludedCallHashes.map { it.value.toHexString() }
)
}
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_account_impl.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.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.common.utils.multisig
import io.novafoundation.nova.common.utils.numberConstant
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigValidationsRepository
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.multisig
import io.novafoundation.nova.feature_account_impl.data.multisig.blockhain.multisigs
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Inject
import javax.inject.Named
@FeatureScope
class RealMultisigValidationsRepository @Inject constructor(
private val chainRegistry: ChainRegistry,
@Named(REMOTE_STORAGE_SOURCE) private val storageDataSource: StorageDataSource
) : MultisigValidationsRepository {
override suspend fun getMultisigDepositBase(chainId: ChainId): BalanceOf {
return chainRegistry.withRuntime(chainId) {
metadata.multisig().numberConstant("DepositBase")
}
}
override suspend fun getMultisigDepositFactor(chainId: ChainId): BalanceOf {
return chainRegistry.withRuntime(chainId) {
metadata.multisig().numberConstant("DepositFactor")
}
}
override suspend fun hasPendingCallHash(chainId: ChainId, accountIdKey: AccountIdKey, callHash: CallHash): Boolean {
val pendingCallHash = storageDataSource.query(chainId) {
metadata.multisig.multisigs.query(accountIdKey, callHash)
}
return pendingCallHash != null
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_account_impl.data.network.blockchain
interface AccountSubstrateSource {
/**
* @throws NovaException
*/
suspend fun getNodeNetworkType(nodeHost: String): String
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_account_impl.data.network.blockchain
import io.novafoundation.nova.common.data.network.rpc.SocketSingleRequestExecutor
import io.novasama.substrate_sdk_android.wsrpc.mappers.nonNull
import io.novasama.substrate_sdk_android.wsrpc.mappers.pojo
import io.novasama.substrate_sdk_android.wsrpc.request.base.RpcRequest
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.system.NodeNetworkTypeRequest
class AccountSubstrateSourceImpl(
private val socketRequestExecutor: SocketSingleRequestExecutor
) : AccountSubstrateSource {
override suspend fun getNodeNetworkType(nodeHost: String): String {
val request = NodeNetworkTypeRequest()
return socketRequestExecutor.executeRequest(RpcRequest.Rpc2(request), nodeHost, pojo<String>().nonNull())
}
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_account_impl.data.network.blockchain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindData
import io.novafoundation.nova.common.data.network.runtime.binding.cast
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.castToStructOrNull
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.utils.second
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
import io.novafoundation.nova.feature_account_api.data.model.RootIdentity
import io.novafoundation.nova.feature_account_api.data.model.SuperOf
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
@UseCaseBinding
fun bindIdentity(dynamic: Any?): OnChainIdentity? {
if (dynamic == null) return null
val decoded = dynamic.castIdentityLegacy() ?: dynamic.castToIdentity()
val identityInfo = decoded.get<Struct.Instance>("info") ?: incompatible()
val pgpFingerprint = identityInfo.get<ByteArray?>("pgpFingerprint")
val matrix = bindIdentityData(identityInfo, "riot", onIncompatibleField = null)
?: bindIdentityData(identityInfo, "matrix", onIncompatibleField = null)
return RootIdentity(
display = bindIdentityData(identityInfo, "display"),
legal = bindIdentityData(identityInfo, "legal"),
web = bindIdentityData(identityInfo, "web"),
matrix = matrix,
email = bindIdentityData(identityInfo, "email"),
pgpFingerprint = pgpFingerprint?.toHexString(withPrefix = true),
image = bindIdentityData(identityInfo, "image"),
twitter = bindIdentityData(identityInfo, "twitter")
)
}
private fun Any?.castIdentityLegacy(): Struct.Instance? {
return this.castToStructOrNull()
}
private fun Any?.castToIdentity(): Struct.Instance {
return this.castToList()
.first()
.castToStruct()
}
@UseCaseBinding
fun bindSuperOf(decoded: Any?): SuperOf? {
if (decoded == null) return null
val asList = decoded.castToList()
val parentId: ByteArray = asList.first().cast()
return SuperOf(
parentId = parentId,
childName = bindData(asList.second()).asString()
)
}
@HelperBinding
fun bindIdentityData(
identityInfo: Struct.Instance,
field: String,
onIncompatibleField: (() -> Unit)? = { incompatible() }
): String? {
val value = identityInfo.get<Any?>(field)
?: onIncompatibleField?.invoke()
?: return null
return bindData(value).asString()
}
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_account_impl.data.proxy
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.utils.zipWithPrevious
import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
class RealMetaAccountsUpdatesRegistry(
private val preferences: Preferences
) : MetaAccountsUpdatesRegistry {
private val KEY = "meta_accounts_changes"
override fun addMetaIds(ids: List<Long>) {
val metaIdsSet = getUpdates()
.toMutableSet()
metaIdsSet.addAll(ids)
val metaIdsJoinedToString = metaIdsSet.joinToString(",")
if (metaIdsJoinedToString.isNotEmpty()) {
preferences.putString(KEY, metaIdsJoinedToString)
}
}
override fun observeUpdates(): Flow<Set<Long>> {
return preferences.keyFlow(KEY)
.map { getUpdates() }
}
override fun getUpdates(): Set<Long> {
val metaIds = preferences.getString(KEY)
if (metaIds.isNullOrEmpty()) return mutableSetOf()
return metaIds.split(",")
.map { it.toLong() }
.toMutableSet()
}
override fun remove(ids: List<Long>) {
val metaIdsSet = getUpdates()
.toMutableSet()
metaIdsSet.removeAll(ids)
val metaIdsJoinedToString = metaIdsSet.joinToString(",")
if (metaIdsJoinedToString.isEmpty()) {
preferences.removeField(KEY)
} else {
preferences.putString(KEY, metaIdsJoinedToString)
}
}
override fun clear() {
preferences.removeField(KEY)
}
override fun observeUpdatesExist(): Flow<Boolean> {
return preferences.keyFlow(KEY)
.map { hasUpdates() }
}
override fun observeLastConsumedUpdatesMetaIds(): Flow<Set<Long>> {
return observeUpdates().zipWithPrevious()
.filter { (old, new) -> !old.isNullOrEmpty() && new.isEmpty() } // Check if updates was consumed
.map { (old, _) -> old.orEmpty() } // Emmit old ids as consumed updates
.distinctUntilChanged()
}
override fun hasUpdates(): Boolean {
return preferences.contains(KEY)
}
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_account_impl.data.proxy.network.api
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
import io.novafoundation.nova.feature_account_impl.data.proxy.network.api.request.FindProxiesRequest
import io.novafoundation.nova.feature_account_impl.data.proxy.network.api.response.FindProxiesResponse
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
interface FindProxiesApi {
@POST
suspend fun findProxies(@Url url: String, @Body body: FindProxiesRequest): SubQueryResponse<FindProxiesResponse>
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_account_impl.data.proxy.network.api.request
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.toHexWithPrefix
import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters
class FindProxiesRequest(
accountIds: Collection<AccountIdKey>
) : SubQueryFilters {
@Transient
private val accountIdsHex = accountIds.map { it.toHexWithPrefix() }
val query = """
query {
proxieds(
filter: {
${"proxyAccountId" presentIn accountIdsHex},
${"delay" equalTo 0}
}) {
nodes {
chainId
type
proxyAccountId
accountId
}
}
}
""".trimIndent()
}
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_account_impl.data.proxy.network.api.response
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
import io.novafoundation.nova.common.utils.HexString
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class FindProxiesResponse(
val proxieds: SubQueryNodes<ProxiedRemote>
)
class ProxiedRemote(
val accountId: HexString,
val type: String,
val proxyAccountId: HexString,
val chainId: ChainId,
)
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_account_impl.data.proxy.repository
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.fromHexOrNull
import io.novafoundation.nova.common.data.config.GlobalConfigDataSource
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.removeHexPrefix
import io.novafoundation.nova.feature_account_impl.data.proxy.network.api.FindProxiesApi
import io.novafoundation.nova.feature_account_impl.data.proxy.network.api.request.FindProxiesRequest
import io.novafoundation.nova.feature_account_impl.data.proxy.network.api.response.FindProxiesResponse
import io.novafoundation.nova.feature_account_impl.data.proxy.repository.model.MultiChainProxy
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novafoundation.nova.feature_proxy_api.domain.model.fromString
import javax.inject.Inject
interface MultiChainProxyRepository {
suspend fun getProxies(accountIds: Collection<AccountIdKey>): List<MultiChainProxy>
}
@FeatureScope
class RealMultiChainProxyRepository @Inject constructor(
private val proxiesApi: FindProxiesApi,
private val globalConfigDataSource: GlobalConfigDataSource
) : MultiChainProxyRepository {
override suspend fun getProxies(accountIds: Collection<AccountIdKey>): List<MultiChainProxy> {
val globalConfig = globalConfigDataSource.getGlobalConfig()
val request = FindProxiesRequest(accountIds)
return proxiesApi.findProxies(globalConfig.proxyApiUrl, request).toDomain()
}
private fun SubQueryResponse<FindProxiesResponse>.toDomain(): List<MultiChainProxy> {
return data.proxieds.nodes.mapNotNull { proxiedNode ->
MultiChainProxy(
chainId = proxiedNode.chainId.removeHexPrefix(),
proxyType = ProxyType.fromString(proxiedNode.type),
proxied = AccountIdKey.fromHexOrNull(proxiedNode.accountId) ?: return@mapNotNull null,
proxy = AccountIdKey.fromHexOrNull(proxiedNode.proxyAccountId) ?: return@mapNotNull null
)
}
}
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_account_impl.data.proxy.repository.model
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
import io.novafoundation.nova.common.utils.HexString
class FindMultisigsResponse(
val accounts: SubQueryNodes<MultisigRemote>
)
class MultisigRemote(
val id: HexString,
val threshold: Int,
val signatories: SubQueryNodes<SignatoryRemoteWrapper>
) {
class SignatoryRemoteWrapper(val signatory: SignatoryRemote)
class SignatoryRemote(
val id: HexString
)
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_account_impl.data.proxy.repository.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class MultiChainProxy(
val chainId: ChainId,
val proxied: AccountIdKey,
val proxy: AccountIdKey,
val proxyType: ProxyType,
)
@@ -0,0 +1,396 @@
package io.novafoundation.nova.feature_account_impl.data.repository
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.data.secrets.v2.getAccountSecrets
import io.novafoundation.nova.common.resources.LanguagesHolder
import io.novafoundation.nova.common.utils.mapList
import io.novafoundation.nova.common.utils.networkType
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core.model.Language
import io.novafoundation.nova.core.model.Network
import io.novafoundation.nova.core.model.Node
import io.novafoundation.nova.core_db.dao.AccountDao
import io.novafoundation.nova.core_db.dao.NodeDao
import io.novafoundation.nova.core_db.model.AccountLocal
import io.novafoundation.nova.core_db.model.NodeLocal
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus.Event
import io.novafoundation.nova.feature_account_api.data.events.combineBusEvents
import io.novafoundation.nova.feature_account_api.data.secrets.keypair
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.Account
import io.novafoundation.nova.feature_account_api.domain.model.AuthType
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.feature_account_api.domain.model.addressIn
import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress
import io.novafoundation.nova.feature_account_api.domain.model.multiChainEncryptionIn
import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn
import io.novafoundation.nova.feature_account_api.domain.model.substrateMultiChainEncryption
import io.novafoundation.nova.feature_account_impl.data.mappers.mapNodeLocalToNode
import io.novafoundation.nova.feature_account_impl.data.network.blockchain.AccountSubstrateSource
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.runtime.ext.genesisHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.encrypt.json.JsonEncoder
import io.novasama.substrate_sdk_android.encrypt.mnemonic.Mnemonic
import io.novasama.substrate_sdk_android.encrypt.mnemonic.MnemonicCreator
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class AccountRepositoryImpl(
private val accountDataSource: AccountDataSource,
private val accountDao: AccountDao,
private val nodeDao: NodeDao,
private val JsonEncoder: JsonEncoder,
private val languagesHolder: LanguagesHolder,
private val accountSubstrateSource: AccountSubstrateSource,
private val secretStoreV2: SecretStoreV2,
private val metaAccountChangesEventBus: MetaAccountChangesEventBus
) : AccountRepository {
override fun getEncryptionTypes(): List<CryptoType> {
return CryptoType.values().toList()
}
override suspend fun getNode(nodeId: Int): Node {
return withContext(Dispatchers.IO) {
val node = nodeDao.getNodeById(nodeId)
mapNodeLocalToNode(node)
}
}
override suspend fun getSelectedNodeOrDefault(): Node {
return accountDataSource.getSelectedNode() ?: mapNodeLocalToNode(nodeDao.getFirstNode())
}
override suspend fun selectNode(node: Node) {
accountDataSource.saveSelectedNode(node)
}
override suspend fun getDefaultNode(networkType: Node.NetworkType): Node {
return mapNodeLocalToNode(nodeDao.getDefaultNodeFor(networkType.ordinal))
}
override suspend fun selectAccount(account: Account, newNode: Node?) {
accountDataSource.saveSelectedAccount(account)
when {
newNode != null -> {
require(account.network.type == newNode.networkType) {
"Account network type is not the same as chosen node type"
}
selectNode(newNode)
}
account.network.type != accountDataSource.getSelectedNode()?.networkType -> {
val defaultNode = getDefaultNode(account.address.networkType())
selectNode(defaultNode)
}
}
}
override suspend fun getSelectedMetaAccount(): MetaAccount {
return accountDataSource.getSelectedMetaAccount()
}
override suspend fun getMetaAccountsByIds(metaIds: List<Long>): List<MetaAccount> {
return accountDataSource.getMetaAccountsByIds(metaIds)
}
override suspend fun getAvailableMetaIdsFromSet(metaIds: Set<Long>): Set<Long> {
val availableMetaIds = accountDataSource.getActiveMetaIds()
return metaIds.intersect(availableMetaIds)
}
override suspend fun getMetaAccount(metaId: Long): MetaAccount {
return accountDataSource.getMetaAccount(metaId)
}
override fun metaAccountFlow(metaId: Long): Flow<MetaAccount> {
return accountDataSource.metaAccountFlow(metaId)
}
override fun selectedMetaAccountFlow(): Flow<MetaAccount> {
return accountDataSource.selectedMetaAccountFlow()
}
override suspend fun findMetaAccount(accountId: ByteArray, chainId: String): MetaAccount? {
return accountDataSource.findMetaAccount(accountId, chainId)
}
override suspend fun accountNameFor(accountId: AccountId, chainId: String): String? {
return accountDataSource.accountNameFor(accountId, chainId)
}
override suspend fun hasActiveMetaAccounts(): Boolean {
return accountDataSource.hasActiveMetaAccounts()
}
override fun allMetaAccountsFlow(): Flow<List<MetaAccount>> {
return accountDataSource.allMetaAccountsFlow()
}
override fun activeMetaAccountsFlow(): Flow<List<MetaAccount>> {
return accountDataSource.activeMetaAccountsFlow()
}
override fun metaAccountBalancesFlow(): Flow<List<MetaAccountAssetBalance>> {
return accountDataSource.metaAccountsWithBalancesFlow()
}
override fun metaAccountBalancesFlow(metaId: Long): Flow<List<MetaAccountAssetBalance>> {
return accountDataSource.metaAccountBalancesFlow(metaId)
}
override suspend fun selectMetaAccount(metaId: Long) {
return accountDataSource.selectMetaAccount(metaId)
}
override suspend fun updateMetaAccountName(metaId: Long, newName: String) = withContext(Dispatchers.Default) {
accountDataSource.updateMetaAccountName(metaId, newName)
val metaAccountType = requireNotNull(accountDataSource.getMetaAccountType(metaId))
val event = Event.AccountNameChanged(metaId, metaAccountType)
metaAccountChangesEventBus.notify(event, source = null)
}
override suspend fun isAccountSelected(): Boolean {
return accountDataSource.anyAccountSelected()
}
override suspend fun deleteAccount(metaId: Long) = withContext(Dispatchers.Default) {
val allAffectedMetaAccounts = accountDataSource.deleteMetaAccount(metaId)
val deleteEvents = allAffectedMetaAccounts.map { Event.AccountRemoved(it.metaId, it.type) }
.combineBusEvents() ?: return@withContext
metaAccountChangesEventBus.notify(deleteEvents, source = null)
}
override suspend fun getAccounts(): List<Account> {
return accountDao.getAccounts()
.map { mapAccountLocalToAccount(it) }
}
override suspend fun getAccount(address: String): Account {
val account = accountDao.getAccount(address) ?: throw NoSuchElementException("No account found for address $address")
return mapAccountLocalToAccount(account)
}
override suspend fun getAccountOrNull(address: String): Account? {
return accountDao.getAccount(address)?.let { mapAccountLocalToAccount(it) }
}
override suspend fun getMyAccounts(query: String, chainId: String): Set<Account> {
// return withContext(Dispatchers.Default) {
// accountDao.getAccounts(query, networkType)
// .map { mapAccountLocalToAccount(it) }
// .toSet()
// }
return emptySet() // TODO wallet
}
override suspend fun isCodeSet(): Boolean {
return accountDataSource.getPinCode() != null
}
override suspend fun savePinCode(code: String) {
return accountDataSource.savePinCode(code)
}
override suspend fun getPinCode(): String? {
return accountDataSource.getPinCode()
}
override suspend fun generateMnemonic(): Mnemonic {
return MnemonicCreator.randomMnemonic(Mnemonic.Length.TWELVE)
}
override fun isBiometricEnabledFlow(): Flow<Boolean> {
return accountDataSource.getAuthTypeFlow().map { it == AuthType.BIOMETRY }
}
override fun isBiometricEnabled(): Boolean {
return accountDataSource.getAuthType() == AuthType.BIOMETRY
}
override fun setBiometricOn() {
return accountDataSource.saveAuthType(AuthType.BIOMETRY)
}
override fun setBiometricOff() {
return accountDataSource.saveAuthType(AuthType.PINCODE)
}
override suspend fun updateAccountsOrdering(accountOrdering: List<MetaAccountOrdering>) {
return accountDataSource.updateAccountPositions(accountOrdering)
}
override suspend fun generateRestoreJson(
metaAccount: MetaAccount,
chain: Chain,
password: String,
): String {
return withContext(Dispatchers.Default) {
val accountId = metaAccount.accountIdIn(chain)!!
val address = metaAccount.addressIn(chain)!!
val secrets = secretStoreV2.getAccountSecrets(metaAccount.id, accountId)
JsonEncoder.generate(
keypair = secrets.keypair(chain),
password = password,
name = metaAccount.name,
multiChainEncryption = metaAccount.multiChainEncryptionIn(chain)!!,
genesisHash = chain.genesisHash.orEmpty(),
address = address
)
}
}
override suspend fun generateRestoreJson(
metaAccount: MetaAccount,
password: String,
): String {
return withContext(Dispatchers.Default) {
val secrets = secretStoreV2.getMetaAccountSecrets(metaAccount.id)!!
JsonEncoder.generate(
keypair = secrets.keypair(ethereum = false),
password = password,
name = metaAccount.name,
multiChainEncryption = metaAccount.substrateMultiChainEncryption()!!,
genesisHash = "",
address = metaAccount.defaultSubstrateAddress!!
)
}
}
override suspend fun isAccountExists(accountId: AccountId, chainId: String): Boolean {
return accountDataSource.accountExists(accountId, chainId)
}
override suspend fun removeDeactivatedMetaAccounts() {
accountDataSource.removeDeactivatedMetaAccounts()
}
override suspend fun getActiveMetaAccounts(): List<MetaAccount> {
return accountDataSource.getActiveMetaAccounts()
}
override suspend fun getAllMetaAccounts(): List<MetaAccount> {
return accountDataSource.getAllMetaAccounts()
}
override suspend fun getActiveMetaAccountsQuantity(): Int {
return accountDataSource.getActiveMetaAccountsQuantity()
}
override fun hasMetaAccountsCountOfTypeFlow(type: LightMetaAccount.Type): Flow<Boolean> {
return accountDataSource.hasMetaAccountsCountOfTypeFlow(type)
}
override suspend fun hasMetaAccountsByType(type: LightMetaAccount.Type): Boolean {
return accountDataSource.hasMetaAccountsByType(type)
}
override suspend fun hasMetaAccountsByType(metaIds: Set<Long>, type: LightMetaAccount.Type): Boolean {
return accountDataSource.hasMetaAccountsByType(metaIds, type)
}
override fun metaAccountsByTypeFlow(type: LightMetaAccount.Type): Flow<List<MetaAccount>> {
return accountDataSource.metaAccountsByTypeFlow(type)
}
override suspend fun hasSecretsAccounts(): Boolean {
return accountDataSource.hasSecretsAccounts()
}
override suspend fun deleteProxiedMetaAccountsByChain(chainId: String) {
accountDataSource.deleteProxiedMetaAccountsByChain(chainId)
}
override fun nodesFlow(): Flow<List<Node>> {
return nodeDao.nodesFlow()
.mapList { mapNodeLocalToNode(it) }
.filter { it.isNotEmpty() }
.flowOn(Dispatchers.Default)
}
override fun getLanguages(): List<Language> {
return languagesHolder.getLanguages()
}
override suspend fun selectedLanguage(): Language {
return accountDataSource.getSelectedLanguage()
}
override suspend fun changeLanguage(language: Language) {
return accountDataSource.changeSelectedLanguage(language)
}
override suspend fun addNode(nodeName: String, nodeHost: String, networkType: Node.NetworkType) {
val nodeLocal = NodeLocal(nodeName, nodeHost, networkType.ordinal, false)
nodeDao.insert(nodeLocal)
}
override suspend fun updateNode(nodeId: Int, newName: String, newHost: String, networkType: Node.NetworkType) {
nodeDao.updateNode(nodeId, newName, newHost, networkType.ordinal)
}
override suspend fun checkNodeExists(nodeHost: String): Boolean {
return nodeDao.checkNodeExists(nodeHost)
}
override suspend fun getNetworkName(nodeHost: String): String {
return accountSubstrateSource.getNodeNetworkType(nodeHost)
}
override suspend fun getAccountsByNetworkType(networkType: Node.NetworkType): List<Account> {
val accounts = accountDao.getAccountsByNetworkType(networkType.ordinal)
return withContext(Dispatchers.Default) {
accounts.map { mapAccountLocalToAccount(it) }
}
}
override suspend fun deleteNode(nodeId: Int) {
return nodeDao.deleteNode(nodeId)
}
override suspend fun createQrAccountContent(chain: Chain, account: MetaAccount): String {
return account.requireAddressIn(chain)
}
private fun mapAccountLocalToAccount(accountLocal: AccountLocal): Account {
val network = getNetworkForType(accountLocal.networkType)
return with(accountLocal) {
Account(
address = address,
name = username,
accountIdHex = publicKey,
cryptoType = CryptoType.values()[accountLocal.cryptoType],
network = network,
position = position
)
}
}
private fun getNetworkForType(networkType: Node.NetworkType): Network {
return Network(networkType)
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_account_impl.data.repository
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.feature_account_api.data.repository.CreateSecretsRepository
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.scale.EncodableStruct
class RealCreateSecretsRepository(
private val accountSecretsFactory: AccountSecretsFactory,
) : CreateSecretsRepository {
override suspend fun createSecretsWithSeed(
seed: ByteArray,
cryptoType: CryptoType,
derivationPath: String?,
isEthereum: Boolean
): EncodableStruct<ChainAccountSecrets> {
val seedString = seed.toHexString()
return accountSecretsFactory.chainAccountSecrets(
derivationPath,
AccountSecretsFactory.AccountSource.Seed(cryptoType, seedString),
isEthereum = isEthereum
).secrets
}
}
@@ -0,0 +1,179 @@
package io.novafoundation.nova.feature_account_impl.data.repository
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.format.getAddressSchemeOrThrow
import io.novafoundation.nova.common.address.get
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.common.utils.filterNotNull
import io.novafoundation.nova.common.utils.groupByToSet
import io.novafoundation.nova.common.utils.hasModule
import io.novafoundation.nova.common.utils.identity
import io.novafoundation.nova.common.utils.mapToSet
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.ChildIdentity
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository
import io.novafoundation.nova.feature_account_impl.data.network.blockchain.bindings.bindIdentity
import io.novafoundation.nova.feature_account_impl.data.network.blockchain.bindings.bindSuperOf
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.ext.addressScheme
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.getChainOrNull
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.metadata.module
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class RealOnChainIdentityRepository(
private val storageDataSource: StorageDataSource,
private val chainRegistry: ChainRegistry
) : OnChainIdentityRepository {
companion object {
private val MULTICHAIN_IDENTITY_CHAINS = listOf(Chain.Geneses.POLKADOT_PEOPLE, Chain.Geneses.KUSAMA_PEOPLE)
}
override suspend fun getIdentitiesFromIdsHex(
chainId: ChainId,
accountIdsHex: Collection<String>
): AccountIdMap<OnChainIdentity?> = withContext(Dispatchers.Default) {
val accountIds = accountIdsHex.map { it.fromHex() }
getIdentitiesFromIds(accountIds, chainId).mapKeys { (accountId, _) -> accountId.value.toHexString() }
}
override suspend fun getIdentitiesFromIds(
accountIds: Collection<AccountId>,
chainId: ChainId
): AccountIdKeyMap<OnChainIdentity?> = withContext(Dispatchers.Default) {
val identityChainId = findIdentityChain(chainId)
val uniqueIds = accountIds.mapToSet(AccountId::intoKey)
getIdentitiesFromIdOnIdentityChain(uniqueIds, identityChainId)
}
private suspend fun getIdentitiesFromIdOnIdentityChain(
accountIds: Set<AccountIdKey>,
identityChainId: ChainId
): AccountIdKeyMap<OnChainIdentity?> = withContext(Dispatchers.Default) {
storageDataSource.query(identityChainId) {
if (!runtime.metadata.hasModule(Modules.IDENTITY)) {
return@query emptyMap()
}
val superOfArguments = accountIds.map { listOf(it.value) }
val superOfValues = runtime.metadata.identity().storage("SuperOf").entries(
keysArguments = superOfArguments,
keyExtractor = { (accountId: AccountId) -> AccountIdKey(accountId) },
binding = { value, _ -> bindSuperOf(value) }
)
val parentIdentityIds = superOfValues.values.filterNotNull().mapToSet { AccountIdKey(it.parentId) }
val parentIdentities = fetchIdentities(parentIdentityIds)
val childIdentities = superOfValues.filterNotNull().mapValues { (_, superOf) ->
val parentIdentity = parentIdentities[superOf.parentId]
parentIdentity?.let { ChildIdentity(superOf.childName, it) }
}
val leftAccountIds = accountIds - childIdentities.keys - parentIdentities.keys
val rootIdentities = fetchIdentities(leftAccountIds.toList())
rootIdentities + childIdentities + parentIdentities
}
}
override suspend fun getIdentityFromId(
chainId: ChainId,
accountId: AccountId
): OnChainIdentity? = withContext(Dispatchers.Default) {
val identityChainId = findIdentityChain(chainId)
storageDataSource.query(identityChainId) {
if (!runtime.metadata.hasModule(Modules.IDENTITY)) {
return@query null
}
val parentRelationship = runtime.metadata.identity().storage("SuperOf").query(accountId, binding = ::bindSuperOf)
if (parentRelationship != null) {
val parentIdentity = fetchIdentity(parentRelationship.parentId)
parentIdentity?.let {
ChildIdentity(parentRelationship.childName, parentIdentity)
}
} else {
fetchIdentity(accountId)
}
}
}
override suspend fun getMultiChainIdentities(
accountIds: Collection<AccountIdKey>
): AccountIdKeyMap<OnChainIdentity?> {
val accountIdsByAddressScheme = accountIds.groupByToSet { it.getAddressSchemeOrThrow() }
val identityChains = MULTICHAIN_IDENTITY_CHAINS.mapNotNull { chainRegistry.getChainOrNull(it) }
return buildMap {
identityChains.onEach { identityChain ->
val addressScheme = identityChain.addressScheme
val accountIdsPerScheme = accountIdsByAddressScheme[addressScheme] ?: return@onEach
val identities = getIdentitiesFromIdOnIdentityChain(accountIdsPerScheme, identityChain.id)
putAll(identities)
// Early return if we already fetched all required identities
if (size == accountIds.size) return@buildMap
}
}
}
override suspend fun getIdentitiesFromAddresses(chain: Chain, accountAddresses: List<String>): AccountAddressMap<OnChainIdentity?> {
val accountIds = accountAddresses.map(chain::accountIdOf)
val identitiesByAccountId = getIdentitiesFromIds(accountIds, chain.id)
return accountAddresses.associateWith { address ->
val accountId = chain.accountIdOf(address)
identitiesByAccountId[accountId]
}
}
private suspend fun StorageQueryContext.fetchIdentities(accountIdsHex: Collection<AccountIdKey>): AccountIdKeyMap<OnChainIdentity?> {
return runtime.metadata.module("Identity").storage("IdentityOf").entries(
keysArguments = accountIdsHex.map { listOf(it.value) },
keyExtractor = { (accountId: AccountId) -> AccountIdKey(accountId) },
binding = { value, _ -> bindIdentity(value) }
)
}
private suspend fun StorageQueryContext.fetchIdentity(accountId: AccountId): OnChainIdentity? {
return runtime.metadata.module("Identity").storage("IdentityOf")
.query(accountId, binding = ::bindIdentity)
}
private suspend fun findIdentityChain(identitiesRequestedOn: ChainId): ChainId {
val requestedChain = chainRegistry.getChain(identitiesRequestedOn)
return findIdentityChain(requestedChain).id
}
private suspend fun findIdentityChain(requestedChain: Chain): Chain {
val identityChain = requestedChain.additional?.identityChain?.let { chainRegistry.getChainOrNull(it) }
return identityChain ?: requestedChain
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_account_impl.data.repository
import io.novafoundation.nova.core_db.dao.MetaAccountDao
class WatchWalletSuggestion(
val name: String,
val substrateAddress: String,
val evmAddress: String?
)
interface WatchOnlyRepository {
fun watchWalletSuggestions(): List<WatchWalletSuggestion>
}
class RealWatchOnlyRepository(
private val accountDao: MetaAccountDao
) : WatchOnlyRepository {
override fun watchWalletSuggestions(): List<WatchWalletSuggestion> {
return listOf(
WatchWalletSuggestion(
name = "\uD83C\uDF0C NOVA",
substrateAddress = "1ChFWeNRLarAPRCTM3bfJmncJbSAbSS9yqjueWz7jX7iTVZ",
evmAddress = "0x7Aa98AEb3AfAcf10021539d5412c7ac6AfE0fb00"
)
)
}
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.toAccountBusEvent
abstract class BaseAddAccountRepository<T>(
private val metaAccountChangesEventBus: MetaAccountChangesEventBus
) : AddAccountRepository<T> {
final override suspend fun addAccount(payload: T): AddAccountResult {
val addAccountResult = addAccountInternal(payload)
addAccountResult.toAccountBusEvent()?.let { metaAccountChangesEventBus.notify(it, source = null) }
return addAccountResult
}
protected abstract suspend fun addAccountInternal(payload: T): AddAccountResult
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novasama.substrate_sdk_android.scale.EncodableStruct
class LocalAddMetaAccountRepository(
metaAccountChangesEventBus: MetaAccountChangesEventBus,
private val metaAccountDao: MetaAccountDao,
private val secretStoreV2: SecretStoreV2
) : BaseAddAccountRepository<LocalAddMetaAccountRepository.Payload>(
metaAccountChangesEventBus
) {
class Payload(val metaAccountLocal: MetaAccountLocal, val secrets: EncodableStruct<MetaAccountSecrets>)
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
val metaId = metaAccountDao.insertMetaAccount(payload.metaAccountLocal)
secretStoreV2.putMetaAccountSecrets(metaId, payload.secrets)
return AddAccountResult.AccountAdded(metaId, LightMetaAccount.Type.SECRETS)
}
}
@@ -0,0 +1,70 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.ledger
import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.dao.updateMetaAccount
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository.Payload
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_impl.data.mappers.AccountMappers
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.BaseAddAccountRepository
import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerDerivationPath
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
class RealGenericLedgerAddAccountRepository(
private val accountDao: MetaAccountDao,
private val secretStoreV2: SecretStoreV2,
private val accountMappers: AccountMappers,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : BaseAddAccountRepository<Payload>(metaAccountChangesEventBus), GenericLedgerAddAccountRepository {
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return when (payload) {
is Payload.NewWallet -> addNewWallet(payload)
is Payload.AddEvmAccount -> addEvmAccount(payload)
}
}
private suspend fun addNewWallet(payload: Payload.NewWallet): AddAccountResult {
val metaAccount = MetaAccountLocal(
substratePublicKey = payload.substrateAccount.publicKey,
substrateCryptoType = mapEncryptionToCryptoType(payload.substrateAccount.encryptionType),
substrateAccountId = payload.substrateAccount.address.toAccountId(),
ethereumPublicKey = payload.evmAccount?.publicKey,
ethereumAddress = payload.evmAccount?.accountId,
name = payload.name,
parentMetaId = null,
isSelected = false,
position = accountDao.nextAccountPosition(),
type = MetaAccountLocal.Type.LEDGER_GENERIC,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = null
)
val metaId = accountDao.insertMetaAccount(metaAccount)
val derivationPathKey = LedgerDerivationPath.genericDerivationPathSecretKey()
secretStoreV2.putAdditionalMetaAccountSecret(metaId, derivationPathKey, payload.substrateAccount.derivationPath)
return AddAccountResult.AccountAdded(metaId, LightMetaAccount.Type.LEDGER)
}
private suspend fun addEvmAccount(payload: Payload.AddEvmAccount): AddAccountResult {
val metaAccountType: LightMetaAccount.Type
accountDao.updateMetaAccount(payload.metaId) { currentMetaAccount ->
metaAccountType = accountMappers.mapMetaAccountTypeFromLocal(currentMetaAccount.type)
currentMetaAccount.addEvmAccount(
ethereumAddress = payload.evmAccount.accountId,
ethereumPublicKey = payload.evmAccount.publicKey
)
}
return AddAccountResult.AccountChanged(payload.metaId, metaAccountType)
}
}
@@ -0,0 +1,89 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.ledger
import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository.Payload
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount.Type
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.BaseAddAccountRepository
import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerDerivationPath
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class RealLegacyLedgerAddAccountRepository(
private val accountDao: MetaAccountDao,
private val chainRegistry: ChainRegistry,
private val secretStoreV2: SecretStoreV2,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : BaseAddAccountRepository<Payload>(metaAccountChangesEventBus), LegacyLedgerAddAccountRepository {
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return when (payload) {
is Payload.MetaAccount -> addMetaAccount(payload)
is Payload.ChainAccount -> addChainAccount(payload)
}
}
private suspend fun addMetaAccount(payload: Payload.MetaAccount): AddAccountResult {
val metaAccount = MetaAccountLocal(
substratePublicKey = null,
substrateCryptoType = null,
substrateAccountId = null,
ethereumPublicKey = null,
ethereumAddress = null,
name = payload.name,
parentMetaId = null,
isSelected = false,
position = accountDao.nextAccountPosition(),
type = MetaAccountLocal.Type.LEDGER,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = null
)
val metaId = accountDao.insertMetaAndChainAccounts(metaAccount) { metaId ->
payload.ledgerChainAccounts.map { (chainId, account) ->
val chain = chainRegistry.getChain(chainId)
ChainAccountLocal(
metaId = metaId,
chainId = chainId,
publicKey = account.publicKey,
accountId = chain.accountIdOf(account.publicKey),
cryptoType = mapEncryptionToCryptoType(account.encryptionType)
)
}
}
payload.ledgerChainAccounts.onEach { (chainId, ledgerAccount) ->
val derivationPathKey = LedgerDerivationPath.legacyDerivationPathSecretKey(chainId)
secretStoreV2.putAdditionalMetaAccountSecret(metaId, derivationPathKey, ledgerAccount.derivationPath)
}
return AddAccountResult.AccountAdded(metaId, type = Type.LEDGER_LEGACY)
}
private suspend fun addChainAccount(payload: Payload.ChainAccount): AddAccountResult {
val chain = chainRegistry.getChain(payload.chainId)
val chainAccount = ChainAccountLocal(
metaId = payload.metaId,
chainId = payload.chainId,
publicKey = payload.ledgerChainAccount.publicKey,
accountId = chain.accountIdOf(payload.ledgerChainAccount.publicKey),
cryptoType = mapEncryptionToCryptoType(payload.ledgerChainAccount.encryptionType)
)
accountDao.insertChainAccount(chainAccount)
val derivationPathKey = LedgerDerivationPath.legacyDerivationPathSecretKey(payload.chainId)
secretStoreV2.putAdditionalMetaAccountSecret(payload.metaId, derivationPathKey, payload.ledgerChainAccount.derivationPath)
return AddAccountResult.AccountChanged(payload.metaId, type = Type.LEDGER)
}
}
@@ -0,0 +1,60 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.paritySigner
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.BaseAddAccountRepository
import io.novasama.substrate_sdk_android.runtime.AccountId
class ParitySignerAddAccountRepository(
private val accountDao: MetaAccountDao,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : BaseAddAccountRepository<ParitySignerAddAccountRepository.Payload>(metaAccountChangesEventBus) {
class Payload(
val name: String,
val substrateAccountId: AccountId,
val variant: PolkadotVaultVariant
)
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
val metaAccount = MetaAccountLocal(
// it is safe to assume that accountId is equal to public key since Parity Signer only uses SR25519
substratePublicKey = payload.substrateAccountId,
substrateAccountId = payload.substrateAccountId,
substrateCryptoType = CryptoType.SR25519,
ethereumPublicKey = null,
ethereumAddress = null,
name = payload.name,
parentMetaId = null,
isSelected = false,
position = accountDao.nextAccountPosition(),
type = payload.variant.asMetaAccountTypeLocal(),
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = null
)
val metaId = accountDao.insertMetaAccount(metaAccount)
return AddAccountResult.AccountAdded(metaId, type = payload.variant.asMetaAccountType())
}
private fun PolkadotVaultVariant.asMetaAccountTypeLocal(): MetaAccountLocal.Type {
return when (this) {
PolkadotVaultVariant.POLKADOT_VAULT -> MetaAccountLocal.Type.POLKADOT_VAULT
PolkadotVaultVariant.PARITY_SIGNER -> MetaAccountLocal.Type.PARITY_SIGNER
}
}
private fun PolkadotVaultVariant.asMetaAccountType(): LightMetaAccount.Type {
return when (this) {
PolkadotVaultVariant.POLKADOT_VAULT -> LightMetaAccount.Type.POLKADOT_VAULT
PolkadotVaultVariant.PARITY_SIGNER -> LightMetaAccount.Type.PARITY_SIGNER
}
}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import android.database.sqlite.SQLiteConstraintException
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountAlreadyExistsException
internal inline fun <R> transformingAccountInsertionErrors(action: () -> R) = try {
action()
} catch (_: SQLiteConstraintException) {
throw AccountAlreadyExistsException()
}
@@ -0,0 +1,55 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType
import io.novafoundation.nova.common.utils.removeHexPrefix
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
import io.novafoundation.nova.feature_account_api.domain.model.ImportJsonMetaData
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novasama.substrate_sdk_android.encrypt.json.JsonDecoder
import io.novasama.substrate_sdk_android.encrypt.model.NetworkTypeIdentifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class JsonAddAccountRepository(
private val accountDataSource: AccountDataSource,
private val accountSecretsFactory: AccountSecretsFactory,
private val JsonDecoder: JsonDecoder,
private val chainRegistry: ChainRegistry,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : SecretsAddAccountRepository<JsonAddAccountRepository.Payload>(
accountDataSource,
accountSecretsFactory,
chainRegistry,
metaAccountChangesEventBus
) {
class Payload(
val json: String,
val password: String,
val addAccountType: AddAccountType
)
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return addSecretsAccount(
derivationPaths = AdvancedEncryption.DerivationPaths.empty(),
addAccountType = payload.addAccountType,
accountSource = AccountSecretsFactory.AccountSource.Json(payload.json, payload.password)
)
}
suspend fun extractJsonMetadata(importJson: String): ImportJsonMetaData = withContext(Dispatchers.Default) {
val importAccountMeta = JsonDecoder.extractImportMetaData(importJson)
with(importAccountMeta) {
val chainId = (networkTypeIdentifier as? NetworkTypeIdentifier.Genesis)?.genesis?.removeHexPrefix()
val cryptoType = mapEncryptionToCryptoType(encryption.encryptionType)
ImportJsonMetaData(name, chainId, cryptoType)
}
}
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.secrets.MnemonicAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.secrets.MnemonicAddAccountRepository.Payload
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class RealMnemonicAddAccountRepository(
accountDataSource: AccountDataSource,
accountSecretsFactory: AccountSecretsFactory,
chainRegistry: ChainRegistry,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : SecretsAddAccountRepository<Payload>(
accountDataSource,
accountSecretsFactory,
chainRegistry,
metaAccountChangesEventBus
),
MnemonicAddAccountRepository {
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return addSecretsAccount(
derivationPaths = payload.advancedEncryption.derivationPaths,
addAccountType = payload.addAccountType,
accountSource = AccountSecretsFactory.AccountSource.Mnemonic(
cryptoType = pickCryptoType(payload.addAccountType, payload.advancedEncryption),
mnemonic = payload.mnemonic
)
)
}
}
@@ -0,0 +1,99 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.BaseAddAccountRepository
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
abstract class SecretsAddAccountRepository<T>(
private val accountDataSource: AccountDataSource,
private val accountSecretsFactory: AccountSecretsFactory,
private val chainRegistry: ChainRegistry,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : BaseAddAccountRepository<T>(metaAccountChangesEventBus) {
protected suspend fun pickCryptoType(addAccountType: AddAccountType, advancedEncryption: AdvancedEncryption): CryptoType {
val cryptoType = if (addAccountType is AddAccountType.ChainAccount && chainRegistry.getChain(addAccountType.chainId).isEthereumBased) {
advancedEncryption.ethereumCryptoType
} else {
advancedEncryption.substrateCryptoType
}
requireNotNull(cryptoType) { "Expected crypto type was null" }
return cryptoType
}
protected suspend fun addSecretsAccount(
derivationPaths: AdvancedEncryption.DerivationPaths,
addAccountType: AddAccountType,
accountSource: AccountSecretsFactory.AccountSource
): AddAccountResult = withContext(Dispatchers.Default) {
when (addAccountType) {
is AddAccountType.MetaAccount -> {
addMetaAccount(derivationPaths, accountSource, addAccountType)
}
is AddAccountType.ChainAccount -> {
addChainAccount(addAccountType, derivationPaths, accountSource)
}
}
}
private suspend fun addMetaAccount(
derivationPaths: AdvancedEncryption.DerivationPaths,
accountSource: AccountSecretsFactory.AccountSource,
addAccountType: AddAccountType.MetaAccount
): AddAccountResult {
val (secrets, substrateCryptoType) = accountSecretsFactory.metaAccountSecrets(
substrateDerivationPath = derivationPaths.substrate,
ethereumDerivationPath = derivationPaths.ethereum,
accountSource = accountSource
)
val metaId = transformingAccountInsertionErrors {
accountDataSource.insertMetaAccountFromSecrets(
name = addAccountType.name,
substrateCryptoType = substrateCryptoType,
secrets = secrets
)
}
return AddAccountResult.AccountAdded(metaId, LightMetaAccount.Type.SECRETS)
}
private suspend fun addChainAccount(
addAccountType: AddAccountType.ChainAccount,
derivationPaths: AdvancedEncryption.DerivationPaths,
accountSource: AccountSecretsFactory.AccountSource
): AddAccountResult {
val chain = chainRegistry.getChain(addAccountType.chainId)
val derivationPath = if (chain.isEthereumBased) derivationPaths.ethereum else derivationPaths.substrate
val (secrets, cryptoType) = accountSecretsFactory.chainAccountSecrets(
derivationPath = derivationPath,
accountSource = accountSource,
isEthereum = chain.isEthereumBased
)
transformingAccountInsertionErrors {
accountDataSource.insertChainAccount(
metaId = addAccountType.metaId,
chain = chain,
cryptoType = cryptoType,
secrets = secrets
)
}
return AddAccountResult.AccountChanged(addAccountType.metaId, LightMetaAccount.Type.SECRETS)
}
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class SeedAddAccountRepository(
accountDataSource: AccountDataSource,
accountSecretsFactory: AccountSecretsFactory,
chainRegistry: ChainRegistry,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : SecretsAddAccountRepository<SeedAddAccountRepository.Payload>(
accountDataSource,
accountSecretsFactory,
chainRegistry,
metaAccountChangesEventBus
) {
class Payload(
val seed: String,
val advancedEncryption: AdvancedEncryption,
val addAccountType: AddAccountType
)
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return addSecretsAccount(
derivationPaths = payload.advancedEncryption.derivationPaths,
addAccountType = payload.addAccountType,
accountSource = AccountSecretsFactory.AccountSource.Seed(
cryptoType = pickCryptoType(payload.addAccountType, payload.advancedEncryption),
seed = payload.seed
)
)
}
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class SubstrateKeypairAddAccountRepository(
accountDataSource: AccountDataSource,
accountSecretsFactory: AccountSecretsFactory,
chainRegistry: ChainRegistry,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : SecretsAddAccountRepository<SubstrateKeypairAddAccountRepository.Payload>(
accountDataSource,
accountSecretsFactory,
chainRegistry,
metaAccountChangesEventBus
) {
class Payload(
val substrateKeypair: ByteArray,
val advancedEncryption: AdvancedEncryption,
val addAccountType: AddAccountType
)
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return addSecretsAccount(
derivationPaths = payload.advancedEncryption.derivationPaths,
addAccountType = payload.addAccountType,
accountSource = AccountSecretsFactory.AccountSource.EncodedSr25519Keypair(key = payload.substrateKeypair)
)
}
}
@@ -0,0 +1,52 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.BaseAddAccountRepository
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.TrustWalletAddAccountRepository.Payload
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.ChainAccountInsertionData
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.MetaAccountInsertionData
import io.novafoundation.nova.feature_account_impl.data.secrets.TrustWalletSecretsFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import javax.inject.Inject
@FeatureScope
class TrustWalletAddAccountRepository @Inject constructor(
val accountDataSource: AccountDataSource,
val accountSecretsFactory: TrustWalletSecretsFactory,
val chainRegistry: ChainRegistry,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : BaseAddAccountRepository<Payload>(metaAccountChangesEventBus) {
class Payload(
val mnemonic: String,
val addAccountType: AddAccountType.MetaAccount
)
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
val (secrets, chainAccountSecrets, substrateCryptoType) = accountSecretsFactory.metaAccountSecrets(payload.mnemonic)
val metaId = transformingAccountInsertionErrors {
accountDataSource.insertMetaAccountWithChainAccounts(
MetaAccountInsertionData(
name = payload.addAccountType.name,
substrateCryptoType = substrateCryptoType,
secrets = secrets
),
chainAccountSecrets.map { (chainId, derivationPath) ->
ChainAccountInsertionData(
chain = chainRegistry.getChain(chainId),
cryptoType = substrateCryptoType,
secrets = derivationPath
)
}
)
}
return AddAccountResult.AccountAdded(metaId, LightMetaAccount.Type.SECRETS)
}
}
@@ -0,0 +1,74 @@
package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.BaseAddAccountRepository
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
class WatchOnlyAddAccountRepository(
private val accountDao: MetaAccountDao,
metaAccountChangesEventBus: MetaAccountChangesEventBus
) : BaseAddAccountRepository<WatchOnlyAddAccountRepository.Payload>(metaAccountChangesEventBus) {
sealed interface Payload {
class MetaAccount(
val name: String,
val substrateAccountId: AccountId,
val ethereumAccountId: AccountId?
) : Payload
class ChainAccount(
val metaId: Long,
val chainId: ChainId,
val accountId: AccountId
) : Payload
}
override suspend fun addAccountInternal(payload: Payload): AddAccountResult {
return when (payload) {
is Payload.MetaAccount -> addWatchOnlyWallet(payload)
is Payload.ChainAccount -> changeWatchOnlyChainAccount(payload)
}
}
private suspend fun addWatchOnlyWallet(payload: Payload.MetaAccount): AddAccountResult {
val metaAccount = MetaAccountLocal(
substratePublicKey = null,
substrateCryptoType = null,
substrateAccountId = payload.substrateAccountId,
ethereumPublicKey = null,
ethereumAddress = payload.ethereumAccountId,
name = payload.name,
parentMetaId = null,
isSelected = false,
position = accountDao.nextAccountPosition(),
type = MetaAccountLocal.Type.WATCH_ONLY,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = null
)
val metaId = accountDao.insertMetaAccount(metaAccount)
return AddAccountResult.AccountAdded(metaId, LightMetaAccount.Type.WATCH_ONLY)
}
private suspend fun changeWatchOnlyChainAccount(payload: Payload.ChainAccount): AddAccountResult {
val chainAccount = ChainAccountLocal(
metaId = payload.metaId,
chainId = payload.chainId,
accountId = payload.accountId,
cryptoType = null,
publicKey = null
)
accountDao.insertChainAccount(chainAccount)
return AddAccountResult.AccountChanged(payload.metaId, LightMetaAccount.Type.WATCH_ONLY)
}
}
@@ -0,0 +1,123 @@
package io.novafoundation.nova.feature_account_impl.data.repository.datasource
import io.novafoundation.nova.common.data.secrets.v1.SecretStoreV1
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
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.AuthType
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.feature_account_api.domain.model.MetaIdWithType
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.ChainAccountInsertionData
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.MetaAccountInsertionData
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.scale.EncodableStruct
import kotlinx.coroutines.flow.Flow
interface AccountDataSource : SecretStoreV1 {
fun getAuthTypeFlow(): Flow<AuthType>
fun saveAuthType(authType: AuthType)
fun getAuthType(): AuthType
suspend fun savePinCode(pinCode: String)
suspend fun getPinCode(): String?
suspend fun saveSelectedNode(node: Node)
suspend fun getSelectedNode(): Node?
suspend fun anyAccountSelected(): Boolean
suspend fun saveSelectedAccount(account: Account)
suspend fun getSelectedMetaAccount(): MetaAccount
fun selectedMetaAccountFlow(): Flow<MetaAccount>
suspend fun findMetaAccount(accountId: ByteArray, chainId: ChainId): MetaAccount?
suspend fun accountNameFor(accountId: AccountId, chainId: ChainId): String?
fun allMetaAccountsFlow(): Flow<List<MetaAccount>>
fun activeMetaAccountsFlow(): Flow<List<MetaAccount>>
fun metaAccountsWithBalancesFlow(): Flow<List<MetaAccountAssetBalance>>
fun metaAccountBalancesFlow(metaId: Long): Flow<List<MetaAccountAssetBalance>>
suspend fun selectMetaAccount(metaId: Long)
suspend fun updateAccountPositions(accountOrdering: List<MetaAccountOrdering>)
suspend fun getSelectedLanguage(): Language
suspend fun changeSelectedLanguage(language: Language)
suspend fun accountExists(accountId: AccountId, chainId: ChainId): Boolean
suspend fun getMetaAccount(metaId: Long): MetaAccount
suspend fun getMetaAccountType(metaId: Long): LightMetaAccount.Type?
fun metaAccountFlow(metaId: Long): Flow<MetaAccount>
suspend fun updateMetaAccountName(metaId: Long, newName: String)
suspend fun deleteMetaAccount(metaId: Long): List<MetaIdWithType>
suspend fun insertMetaAccountWithChainAccounts(metaAccount: MetaAccountInsertionData, chainAccounts: List<ChainAccountInsertionData>): Long
/**
* @return id of inserted meta account
*/
suspend fun insertMetaAccountFromSecrets(
name: String,
substrateCryptoType: CryptoType,
secrets: EncodableStruct<MetaAccountSecrets>
): Long
suspend fun insertChainAccount(
metaId: Long,
chain: Chain,
cryptoType: CryptoType,
secrets: EncodableStruct<ChainAccountSecrets>
)
suspend fun hasActiveMetaAccounts(): Boolean
fun removeDeactivatedMetaAccounts()
suspend fun getActiveMetaAccounts(): List<MetaAccount>
suspend fun getActiveMetaIds(): Set<Long>
suspend fun getAllMetaAccounts(): List<MetaAccount>
suspend fun getMetaAccountsByIds(metaIds: List<Long>): List<MetaAccount>
fun hasMetaAccountsCountOfTypeFlow(type: LightMetaAccount.Type): Flow<Boolean>
suspend fun getActiveMetaAccountsQuantity(): Int
suspend fun hasSecretsAccounts(): Boolean
suspend fun deleteProxiedMetaAccountsByChain(chainId: String)
fun metaAccountsByTypeFlow(type: LightMetaAccount.Type): Flow<List<MetaAccount>>
suspend fun hasMetaAccountsByType(type: LightMetaAccount.Type): Boolean
suspend fun hasMetaAccountsByType(metaIds: Set<Long>, type: LightMetaAccount.Type): Boolean
}
suspend fun AccountDataSource.getMetaAccountTypeOrThrow(metaId: Long): LightMetaAccount.Type {
return requireNotNull(getMetaAccountType(metaId))
}
@@ -0,0 +1,328 @@
package io.novafoundation.nova.feature_account_impl.data.repository.datasource
import android.util.Log
import io.novafoundation.nova.common.data.secrets.v1.SecretStoreV1
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.KeyPairSchema
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences
import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.mapList
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.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.dao.NodeDao
import io.novafoundation.nova.core_db.dao.withTransaction
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountPositionUpdate
import io.novafoundation.nova.feature_account_api.domain.model.Account
import io.novafoundation.nova.feature_account_api.domain.model.AuthType
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.feature_account_api.domain.model.MetaIdWithType
import io.novafoundation.nova.feature_account_impl.data.mappers.AccountMappers
import io.novafoundation.nova.feature_account_impl.data.mappers.mapMetaAccountTypeToLocal
import io.novafoundation.nova.feature_account_impl.data.mappers.mapMetaAccountWithBalanceFromLocal
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.AccountDataMigration
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.ChainAccountInsertionData
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model.MetaAccountInsertionData
import io.novafoundation.nova.runtime.ext.accountIdOf
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.scale.EncodableStruct
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private const val PREFS_AUTH_TYPE = "auth_type"
private const val PREFS_PIN_CODE = "pin_code"
class AccountDataSourceImpl(
private val preferences: Preferences,
private val encryptedPreferences: EncryptedPreferences,
private val nodeDao: NodeDao,
private val metaAccountDao: MetaAccountDao,
private val accountMappers: AccountMappers,
private val secretStoreV2: SecretStoreV2,
private val secretsMetaAccountLocalFactory: SecretsMetaAccountLocalFactory,
secretStoreV1: SecretStoreV1,
accountDataMigration: AccountDataMigration,
) : AccountDataSource, SecretStoreV1 by secretStoreV1 {
init {
migrateIfNeeded(accountDataMigration)
}
private fun migrateIfNeeded(migration: AccountDataMigration) = async {
if (migration.migrationNeeded()) {
migration.migrate(::saveSecuritySource)
}
}
private val selectedMetaAccountFlow = metaAccountDao.selectedMetaAccountInfoFlow()
.distinctUntilChanged()
.onEach { Log.d("AccountDataSourceImpl", "Current meta account: ${it?.metaAccount?.id}") }
.filterNotNull()
.map(accountMappers::mapMetaAccountLocalToMetaAccount)
.inBackground()
.shareIn(GlobalScope, started = SharingStarted.Eagerly, replay = 1)
override fun getAuthTypeFlow(): Flow<AuthType> {
return preferences.stringFlow(PREFS_AUTH_TYPE).map { savedValue ->
if (savedValue == null) {
AuthType.PINCODE
} else {
AuthType.valueOf(savedValue)
}
}
}
override fun saveAuthType(authType: AuthType) {
preferences.putString(PREFS_AUTH_TYPE, authType.toString())
}
override fun getAuthType(): AuthType {
val savedValue = preferences.getString(PREFS_AUTH_TYPE)
return if (savedValue == null) {
AuthType.PINCODE
} else {
AuthType.valueOf(savedValue)
}
}
override suspend fun savePinCode(pinCode: String) = withContext(Dispatchers.IO) {
encryptedPreferences.putEncryptedString(PREFS_PIN_CODE, pinCode)
}
override suspend fun getPinCode(): String? {
return withContext(Dispatchers.IO) {
encryptedPreferences.getDecryptedString(PREFS_PIN_CODE)
}
}
override suspend fun saveSelectedNode(node: Node) = withContext(Dispatchers.Default) {
nodeDao.switchActiveNode(node.id)
}
override suspend fun getSelectedNode(): Node? = null
override suspend fun anyAccountSelected(): Boolean = metaAccountDao.selectedMetaAccount() != null
override suspend fun saveSelectedAccount(account: Account) = withContext(Dispatchers.Default) {
// TODO remove compatibility stub
}
override suspend fun getSelectedMetaAccount(): MetaAccount {
return selectedMetaAccountFlow.first()
}
override fun selectedMetaAccountFlow(): Flow<MetaAccount> = selectedMetaAccountFlow
override suspend fun findMetaAccount(accountId: ByteArray, chainId: ChainId): MetaAccount? {
return metaAccountDao.getMetaAccountInfo(accountId, chainId)
?.let { accountMappers.mapMetaAccountLocalToMetaAccount(it) }
}
override suspend fun accountNameFor(accountId: AccountId, chainId: ChainId): String? {
return metaAccountDao.metaAccountNameFor(accountId, chainId)
}
override suspend fun getActiveMetaAccounts(): List<MetaAccount> {
val local = metaAccountDao.getMetaAccountsByStatus(MetaAccountLocal.Status.ACTIVE)
return accountMappers.mapMetaAccountsLocalToMetaAccounts(local)
}
override suspend fun getActiveMetaIds(): Set<Long> {
return withContext(Dispatchers.IO) { metaAccountDao.getMetaAccountsIdsByStatus(MetaAccountLocal.Status.ACTIVE).toSet() }
}
override suspend fun getAllMetaAccounts(): List<MetaAccount> {
val local = metaAccountDao.getFullMetaAccounts()
return accountMappers.mapMetaAccountsLocalToMetaAccounts(local)
}
override suspend fun getMetaAccountsByIds(metaIds: List<Long>): List<MetaAccount> {
val localMetaAccounts = metaAccountDao.getMetaAccountsByIds(metaIds)
return accountMappers.mapMetaAccountsLocalToMetaAccounts(localMetaAccounts)
}
override fun hasMetaAccountsCountOfTypeFlow(type: LightMetaAccount.Type): Flow<Boolean> {
return metaAccountDao.hasMetaAccountsCountOfTypeFlow(mapMetaAccountTypeToLocal(type)).distinctUntilChanged()
}
override fun metaAccountsByTypeFlow(type: LightMetaAccount.Type): Flow<List<MetaAccount>> {
return metaAccountDao.observeMetaAccountsByTypeFlow(mapMetaAccountTypeToLocal(type))
.map { accountMappers.mapMetaAccountsLocalToMetaAccounts(it) }
}
override suspend fun hasMetaAccountsByType(type: LightMetaAccount.Type): Boolean {
return metaAccountDao.hasMetaAccountsByType(mapMetaAccountTypeToLocal(type))
}
override suspend fun hasMetaAccountsByType(metaIds: Set<Long>, type: LightMetaAccount.Type): Boolean {
return metaAccountDao.hasMetaAccountsByType(metaIds, mapMetaAccountTypeToLocal(type))
}
override suspend fun getActiveMetaAccountsQuantity(): Int {
return metaAccountDao.getMetaAccountsQuantityByStatus(MetaAccountLocal.Status.ACTIVE)
}
override suspend fun deleteProxiedMetaAccountsByChain(chainId: String) {
return metaAccountDao.deleteProxiedMetaAccountsByChain(chainId)
}
override suspend fun hasSecretsAccounts(): Boolean {
return metaAccountDao.hasMetaAccountsByType(MetaAccountLocal.Type.SECRETS)
}
override fun allMetaAccountsFlow(): Flow<List<MetaAccount>> {
return metaAccountDao.getJoinedMetaAccountsInfoFlow().map { accountsLocal ->
accountMappers.mapMetaAccountsLocalToMetaAccounts(accountsLocal)
}
}
override fun activeMetaAccountsFlow(): Flow<List<MetaAccount>> {
return metaAccountDao.getJoinedMetaAccountsInfoByStatusFlow(MetaAccountLocal.Status.ACTIVE)
.map { accountsLocal ->
accountMappers.mapMetaAccountsLocalToMetaAccounts(accountsLocal)
}
}
override fun metaAccountsWithBalancesFlow(): Flow<List<MetaAccountAssetBalance>> {
return metaAccountDao.metaAccountsWithBalanceFlow().mapList(::mapMetaAccountWithBalanceFromLocal)
}
override fun metaAccountBalancesFlow(metaId: Long): Flow<List<MetaAccountAssetBalance>> {
return metaAccountDao.metaAccountWithBalanceFlow(metaId).mapList(::mapMetaAccountWithBalanceFromLocal)
}
override suspend fun selectMetaAccount(metaId: Long) {
metaAccountDao.selectMetaAccount(metaId)
}
override suspend fun updateAccountPositions(accountOrdering: List<MetaAccountOrdering>) = withContext(Dispatchers.Default) {
val positionUpdates = accountOrdering.map {
MetaAccountPositionUpdate(id = it.id, position = it.position)
}
metaAccountDao.updatePositions(positionUpdates)
}
override suspend fun getSelectedLanguage(): Language = withContext(Dispatchers.IO) {
preferences.getCurrentLanguage() ?: throw IllegalArgumentException("No language selected")
}
override suspend fun changeSelectedLanguage(language: Language) = withContext(Dispatchers.IO) {
preferences.saveCurrentLanguage(language.iso639Code)
}
override suspend fun accountExists(accountId: AccountId, chainId: ChainId): Boolean {
return metaAccountDao.isMetaAccountExists(accountId, chainId)
}
override suspend fun getMetaAccount(metaId: Long): MetaAccount {
val joinedMetaAccountInfo = metaAccountDao.getJoinedMetaAccountInfo(metaId)
return accountMappers.mapMetaAccountLocalToMetaAccount(joinedMetaAccountInfo)
}
override suspend fun getMetaAccountType(metaId: Long): LightMetaAccount.Type? {
return metaAccountDao.getMetaAccountType(metaId)?.let(accountMappers::mapMetaAccountTypeFromLocal)
}
override fun metaAccountFlow(metaId: Long): Flow<MetaAccount> {
return metaAccountDao.metaAccountInfoFlow(metaId).mapNotNull { local ->
local?.let { accountMappers.mapMetaAccountLocalToMetaAccount(it) }
}
}
override suspend fun updateMetaAccountName(metaId: Long, newName: String) {
metaAccountDao.updateName(metaId, newName)
}
override suspend fun deleteMetaAccount(metaId: Long): List<MetaIdWithType> {
val joinedMetaAccountInfo = metaAccountDao.getJoinedMetaAccountInfo(metaId)
val chainAccountIds = joinedMetaAccountInfo.chainAccounts.map(ChainAccountLocal::accountId)
val allAffectedMetaAccounts = metaAccountDao.delete(metaId)
secretStoreV2.clearMetaAccountSecrets(metaId, chainAccountIds)
return allAffectedMetaAccounts.map { MetaIdWithType(it.id, accountMappers.mapMetaAccountTypeFromLocal(it.type)) }
}
override suspend fun insertMetaAccountWithChainAccounts(
metaAccount: MetaAccountInsertionData,
chainAccounts: List<ChainAccountInsertionData>
) = withContext(Dispatchers.Default) {
metaAccountDao.withTransaction {
val metaId = insertMetaAccountFromSecrets(metaAccount.name, metaAccount.substrateCryptoType, metaAccount.secrets)
chainAccounts.forEach { insertChainAccount(metaId, it.chain, it.cryptoType, it.secrets) }
metaId
}
}
override suspend fun insertMetaAccountFromSecrets(
name: String,
substrateCryptoType: CryptoType,
secrets: EncodableStruct<MetaAccountSecrets>
) = withContext(Dispatchers.Default) {
val metaAccountLocal = secretsMetaAccountLocalFactory.create(name, substrateCryptoType, secrets, metaAccountDao.nextAccountPosition())
val metaId = metaAccountDao.insertMetaAccount(metaAccountLocal)
secretStoreV2.putMetaAccountSecrets(metaId, secrets)
metaId
}
override suspend fun insertChainAccount(
metaId: Long,
chain: Chain,
cryptoType: CryptoType,
secrets: EncodableStruct<ChainAccountSecrets>
) = withContext(Dispatchers.Default) {
val publicKey = secrets[ChainAccountSecrets.Keypair][KeyPairSchema.PublicKey]
val accountId = chain.accountIdOf(publicKey)
val chainAccountLocal = ChainAccountLocal(
metaId = metaId,
chainId = chain.id,
publicKey = publicKey,
accountId = accountId,
cryptoType = cryptoType
)
metaAccountDao.insertChainAccount(chainAccountLocal)
secretStoreV2.putChainAccountSecrets(metaId, accountId, secrets)
}
override suspend fun hasActiveMetaAccounts(): Boolean {
return metaAccountDao.hasMetaAccountsByStatus(MetaAccountLocal.Status.ACTIVE)
}
override fun removeDeactivatedMetaAccounts() {
metaAccountDao.removeMetaAccountsByStatus(MetaAccountLocal.Status.DEACTIVATED)
}
private inline fun async(crossinline action: suspend () -> Unit) {
GlobalScope.launch(Dispatchers.Default) {
action()
}
}
}
@@ -0,0 +1,49 @@
package io.novafoundation.nova.feature_account_impl.data.repository.datasource
import io.novafoundation.nova.common.data.secrets.v2.KeyPairSchema
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.common.utils.substrateAccountId
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
import io.novasama.substrate_sdk_android.extensions.toAccountId
import io.novasama.substrate_sdk_android.scale.EncodableStruct
interface SecretsMetaAccountLocalFactory {
fun create(
name: String,
substrateCryptoType: CryptoType,
secrets: EncodableStruct<MetaAccountSecrets>,
accountSortPosition: Int,
): MetaAccountLocal
}
class RealSecretsMetaAccountLocalFactory : SecretsMetaAccountLocalFactory {
override fun create(
name: String,
substrateCryptoType: CryptoType,
secrets: EncodableStruct<MetaAccountSecrets>,
accountSortPosition: Int,
): MetaAccountLocal {
val substratePublicKey = secrets[MetaAccountSecrets.SubstrateKeypair][KeyPairSchema.PublicKey]
val ethereumPublicKey = secrets[MetaAccountSecrets.EthereumKeypair]?.get(KeyPairSchema.PublicKey)
return MetaAccountLocal(
substratePublicKey = substratePublicKey,
substrateCryptoType = substrateCryptoType,
substrateAccountId = substratePublicKey.substrateAccountId(),
ethereumPublicKey = ethereumPublicKey,
ethereumAddress = ethereumPublicKey?.asEthereumPublicKey()?.toAccountId()?.value,
name = name,
parentMetaId = null,
isSelected = false,
position = accountSortPosition,
type = MetaAccountLocal.Type.SECRETS,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = null
)
}
}
@@ -0,0 +1,93 @@
package io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration
import android.annotation.SuppressLint
import io.novafoundation.nova.common.data.secrets.v1.Keypair
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences
import io.novafoundation.nova.core.model.SecuritySource
import io.novafoundation.nova.core_db.dao.AccountDao
import io.novafoundation.nova.core_db.model.AccountLocal
import io.novasama.substrate_sdk_android.encrypt.mnemonic.MnemonicCreator
import io.novasama.substrate_sdk_android.scale.Schema
import io.novasama.substrate_sdk_android.scale.byteArray
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.bouncycastle.util.encoders.Hex
private const val PREFS_PRIVATE_KEY = "private_%s"
private const val PREFS_SEED_MASK = "seed_%s"
private const val PREFS_DERIVATION_MASK = "derivation_%s"
private const val PREFS_ENTROPY_MASK = "entropy_%s"
private const val PREFS_MIGRATED_FROM_0_4_1_TO_1_0_0 = "migrated_from_0_4_1_to_1_0_0"
private object ScaleSigningData : Schema<ScaleSigningData>() {
val PrivateKey by byteArray()
val PublicKey by byteArray()
val Nonce by byteArray().optional()
}
typealias SaveSourceCallback = suspend (String, SecuritySource) -> Unit
@SuppressLint("CheckResult")
class AccountDataMigration(
private val preferences: Preferences,
private val encryptedPreferences: EncryptedPreferences,
private val accountsDao: AccountDao,
) {
suspend fun migrationNeeded(): Boolean = withContext(Dispatchers.Default) {
val migrated = preferences.getBoolean(PREFS_MIGRATED_FROM_0_4_1_TO_1_0_0, false)
!migrated
}
suspend fun migrate(saveSourceCallback: SaveSourceCallback) = withContext(Dispatchers.Default) {
val accounts = accountsDao.getAccounts()
migrateAllAccounts(accounts, saveSourceCallback)
preferences.putBoolean(PREFS_MIGRATED_FROM_0_4_1_TO_1_0_0, true)
}
private suspend fun migrateAllAccounts(accounts: List<AccountLocal>, saveSourceCallback: SaveSourceCallback) {
accounts.forEach { migrateAccount(it.address, saveSourceCallback) }
}
private suspend fun migrateAccount(accountAddress: String, saveSourceCallback: SaveSourceCallback) {
val oldKey = PREFS_PRIVATE_KEY.format(accountAddress)
val oldRaw = encryptedPreferences.getDecryptedString(oldKey) ?: return
val data = ScaleSigningData.read(oldRaw)
val keypair = Keypair(
publicKey = data[ScaleSigningData.PublicKey],
privateKey = data[ScaleSigningData.PrivateKey],
nonce = data[ScaleSigningData.Nonce]
)
val seedKey = PREFS_SEED_MASK.format(accountAddress)
val seedValue = encryptedPreferences.getDecryptedString(seedKey)
val seed = seedValue?.let { Hex.decode(it) }
val derivationKey = PREFS_DERIVATION_MASK.format(accountAddress)
val derivationPath = encryptedPreferences.getDecryptedString(derivationKey)
val entropyKey = PREFS_ENTROPY_MASK.format(accountAddress)
val entropyValue = encryptedPreferences.getDecryptedString(entropyKey)
val entropy = entropyValue?.let { Hex.decode(it) }
val securitySource = if (entropy != null) {
val mnemonic = MnemonicCreator.fromEntropy(entropy)
SecuritySource.Specified.Mnemonic(seed, keypair, mnemonic.words, derivationPath)
} else {
if (seed != null) {
SecuritySource.Specified.Seed(seed, keypair, derivationPath)
} else {
SecuritySource.Unspecified(keypair)
}
}
saveSourceCallback(accountAddress, securitySource)
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.scale.EncodableStruct
class ChainAccountInsertionData(
val chain: Chain,
val cryptoType: CryptoType,
val secrets: EncodableStruct<ChainAccountSecrets>
)
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.model
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.core.model.CryptoType
import io.novasama.substrate_sdk_android.scale.EncodableStruct
class MetaAccountInsertionData(
val name: String,
val substrateCryptoType: CryptoType,
val secrets: EncodableStruct<MetaAccountSecrets>
)
@@ -0,0 +1,168 @@
package io.novafoundation.nova.feature_account_impl.data.secrets
import io.novafoundation.nova.common.data.mappers.mapCryptoTypeToEncryption
import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType
import io.novafoundation.nova.common.data.network.runtime.binding.cast
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.mapKeypairStructToKeypair
import io.novafoundation.nova.common.utils.castOrNull
import io.novafoundation.nova.common.utils.deriveSeed32
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.feature_account_api.data.derivationPath.DerivationPathDecoder
import io.novasama.substrate_sdk_android.encrypt.EncryptionType
import io.novasama.substrate_sdk_android.encrypt.MultiChainEncryption
import io.novasama.substrate_sdk_android.encrypt.json.JsonDecoder
import io.novasama.substrate_sdk_android.encrypt.junction.JunctionDecoder
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.Bip32EcdsaKeypairFactory
import io.novasama.substrate_sdk_android.encrypt.keypair.generate
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.Sr25519SubstrateKeypairFactory
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.SubstrateKeypairFactory
import io.novasama.substrate_sdk_android.encrypt.mnemonic.MnemonicCreator
import io.novasama.substrate_sdk_android.encrypt.seed.SeedFactory
import io.novasama.substrate_sdk_android.encrypt.seed.bip39.Bip39SeedFactory
import io.novasama.substrate_sdk_android.encrypt.seed.substrate.SubstrateSeedFactory
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.scale.EncodableStruct
import io.novasama.substrate_sdk_android.scale.Schema
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AccountSecretsFactory(
private val JsonDecoder: JsonDecoder
) {
sealed class AccountSource {
class Mnemonic(val cryptoType: CryptoType, val mnemonic: String) : AccountSource()
class Seed(val cryptoType: CryptoType, val seed: String) : AccountSource()
class Json(val json: String, val password: String) : AccountSource()
class EncodedSr25519Keypair(val key: ByteArray) : AccountSource()
}
sealed class SecretsError : Exception() {
class NotValidEthereumCryptoType : SecretsError()
class NotValidSubstrateCryptoType : SecretsError()
}
data class Result<S : Schema<S>>(val secrets: EncodableStruct<S>, val cryptoType: CryptoType)
suspend fun chainAccountSecrets(
derivationPath: String?,
accountSource: AccountSource,
isEthereum: Boolean,
): Result<ChainAccountSecrets> = withContext(Dispatchers.Default) {
val mnemonicWords = accountSource.castOrNull<AccountSource.Mnemonic>()?.mnemonic
val entropy = mnemonicWords?.let(MnemonicCreator::fromWords)?.entropy
val decodedDerivationPath = decodeDerivationPath(derivationPath, ethereum = isEthereum)
val decodedJson = accountSource.castOrNull<AccountSource.Json>()?.let { jsonSource ->
JsonDecoder.decode(jsonSource.json, jsonSource.password).also {
// only allow Ethereum JSONs for ethereum chains
if (isEthereum && it.multiChainEncryption != MultiChainEncryption.Ethereum) {
throw SecretsError.NotValidEthereumCryptoType()
}
// only allow Substrate JSONs for substrate chains
if (!isEthereum && it.multiChainEncryption == MultiChainEncryption.Ethereum) {
throw SecretsError.NotValidSubstrateCryptoType()
}
}
}
val encryptionType = when (accountSource) {
is AccountSource.Mnemonic -> mapCryptoTypeToEncryption(accountSource.cryptoType)
is AccountSource.Seed -> mapCryptoTypeToEncryption(accountSource.cryptoType)
is AccountSource.Json -> decodedJson!!.multiChainEncryption.encryptionType
is AccountSource.EncodedSr25519Keypair -> EncryptionType.SR25519
}
val seed = when (accountSource) {
is AccountSource.Mnemonic -> deriveSeed(accountSource.mnemonic, decodedDerivationPath?.password, ethereum = isEthereum).seed
is AccountSource.Seed -> accountSource.seed.fromHex()
is AccountSource.Json -> null
is AccountSource.EncodedSr25519Keypair -> null
}
val keypair = when {
seed != null -> {
val junctions = decodedDerivationPath?.junctions.orEmpty()
if (isEthereum) {
Bip32EcdsaKeypairFactory.generate(seed, junctions)
} else {
SubstrateKeypairFactory.generate(encryptionType, seed, junctions)
}
}
decodedJson != null -> {
decodedJson.keypair
}
else -> {
val encodedSr25519Keypair = accountSource.cast<AccountSource.EncodedSr25519Keypair>()
Sr25519SubstrateKeypairFactory.createKeypairFromSecret(encodedSr25519Keypair.key)
}
}
val secrets = ChainAccountSecrets(
keyPair = keypair,
entropy = entropy,
seed = seed,
derivationPath = derivationPath,
)
Result(secrets = secrets, cryptoType = mapEncryptionToCryptoType(encryptionType))
}
suspend fun metaAccountSecrets(
substrateDerivationPath: String?,
ethereumDerivationPath: String?,
accountSource: AccountSource,
): Result<MetaAccountSecrets> = withContext(Dispatchers.Default) {
val (substrateSecrets, substrateCryptoType) = chainAccountSecrets(
derivationPath = substrateDerivationPath,
accountSource = accountSource,
isEthereum = false
)
val ethereumKeypair = accountSource.castOrNull<AccountSource.Mnemonic>()?.let {
val decodedEthereumDerivationPath = decodeDerivationPath(ethereumDerivationPath, ethereum = true)
val seed = deriveSeed(it.mnemonic, password = decodedEthereumDerivationPath?.password, ethereum = true).seed
Bip32EcdsaKeypairFactory.generate(seed = seed, junctions = decodedEthereumDerivationPath?.junctions.orEmpty())
}
val secrets = MetaAccountSecrets(
entropy = substrateSecrets[ChainAccountSecrets.Entropy],
substrateSeed = substrateSecrets[ChainAccountSecrets.Seed],
substrateKeyPair = mapKeypairStructToKeypair(substrateSecrets[ChainAccountSecrets.Keypair]),
substrateDerivationPath = substrateDerivationPath,
ethereumKeypair = ethereumKeypair,
ethereumDerivationPath = ethereumDerivationPath
)
Result(secrets = secrets, cryptoType = substrateCryptoType)
}
private fun deriveSeed(mnemonic: String, password: String?, ethereum: Boolean): SeedFactory.Result {
return if (ethereum) {
Bip39SeedFactory.deriveSeed(mnemonic, password)
} else {
SubstrateSeedFactory.deriveSeed32(mnemonic, password)
}
}
private fun decodeDerivationPath(derivationPath: String?, ethereum: Boolean): JunctionDecoder.DecodeResult? {
return when {
ethereum -> DerivationPathDecoder.decodeEthereumDerivationPath(derivationPath)
else -> DerivationPathDecoder.decodeSubstrateDerivationPath(derivationPath)
}
}
}
@@ -0,0 +1,91 @@
package io.novafoundation.nova.feature_account_impl.data.secrets
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.DEFAULT_DERIVATION_PATH
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.encrypt.junction.BIP32JunctionDecoder
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.Bip32EcdsaKeypairFactory
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.Bip32Ed25519KeypairFactory
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.generate
import io.novasama.substrate_sdk_android.encrypt.seed.SeedFactory
import io.novasama.substrate_sdk_android.encrypt.seed.bip39.Bip39SeedFactory
import io.novasama.substrate_sdk_android.scale.EncodableStruct
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
@FeatureScope
class TrustWalletSecretsFactory @Inject constructor() {
companion object {
private const val TRUST_SUBSTRATE_DERIVATION_PATH = "//44//354//0//0//0"
private fun trustWalletChainAccountDerivationPaths(): Map<ChainId, String> {
return mapOf(
ChainGeneses.KUSAMA to "//44//434//0//0//0",
ChainGeneses.KUSAMA_ASSET_HUB to "//44//434//0//0//0"
)
}
}
suspend fun metaAccountSecrets(mnemonicWords: String): TrustWalletMetaAccountSecrets = withContext(Dispatchers.Default) {
val seedResult = deriveSeed(mnemonicWords)
val secrets = MetaAccountSecrets(
entropy = seedResult.mnemonic.entropy,
substrateDerivationPath = TRUST_SUBSTRATE_DERIVATION_PATH,
substrateSeed = seedResult.seed,
substrateKeyPair = deriveKeypair(seedResult, AddressScheme.SUBSTRATE),
ethereumKeypair = deriveKeypair(seedResult, AddressScheme.EVM),
ethereumDerivationPath = BIP32JunctionDecoder.DEFAULT_DERIVATION_PATH
)
val chainAccountSecrets = trustWalletChainAccountDerivationPaths()
.mapValues { (_, derivationPath) ->
val chainKeyPair = deriveKeypair(seedResult, AddressScheme.SUBSTRATE, derivationPath)
ChainAccountSecrets(
entropy = seedResult.mnemonic.entropy,
seed = seedResult.seed,
derivationPath = derivationPath,
keyPair = chainKeyPair
)
}
TrustWalletMetaAccountSecrets(secrets, chainAccountSecrets, substrateCryptoType = CryptoType.ED25519)
}
private fun deriveSeed(mnemonic: String): SeedFactory.Result {
return Bip39SeedFactory.deriveSeed(mnemonic, password = null)
}
private fun deriveKeypair(
seedResult: SeedFactory.Result,
addressScheme: AddressScheme,
derivationPath: String = getDerivationPath(addressScheme)
): Keypair {
return when (addressScheme) {
AddressScheme.EVM -> Bip32EcdsaKeypairFactory.generate(seedResult.seed, derivationPath)
AddressScheme.SUBSTRATE -> Bip32Ed25519KeypairFactory.generate(seedResult.seed, derivationPath)
}
}
private fun getDerivationPath(addressScheme: AddressScheme): String {
return when (addressScheme) {
AddressScheme.EVM -> BIP32JunctionDecoder.DEFAULT_DERIVATION_PATH
AddressScheme.SUBSTRATE -> TRUST_SUBSTRATE_DERIVATION_PATH
}
}
data class TrustWalletMetaAccountSecrets(
val secrets: EncodableStruct<MetaAccountSecrets>,
val chainAccountSecrets: Map<ChainId, EncodableStruct<ChainAccountSecrets>>,
val substrateCryptoType: CryptoType
)
}
@@ -0,0 +1,99 @@
package io.novafoundation.nova.feature_account_impl.data.signer
import android.util.Log
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
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.ext.accountIdOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.encrypt.EncryptionType
import io.novasama.substrate_sdk_android.encrypt.MultiChainEncryption
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.Bip32EcdsaKeypairFactory
import io.novasama.substrate_sdk_android.encrypt.keypair.generate
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.SubstrateKeypairFactory
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.KeyPairSigner
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckNonce.Companion.setNonce
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.verifySignature.GeneralTransactionSigner
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.verifySignature.VerifySignature.Companion.setVerifySignature
private val FAKE_CRYPTO_TYPE = EncryptionType.ECDSA
/**
* A basic implementation of [NovaSigner] that implements foundation for any signer that
* does not delegate any of the [NovaSigner] methods to the nested signers
*/
abstract class LeafSigner(
override val metaAccount: MetaAccount,
) : NovaSigner, GeneralTransactionSigner {
override suspend fun getSigningHierarchy(): SubmissionHierarchy {
return SubmissionHierarchy(metaAccount, callExecutionType())
}
// All leaf signers are immediate atm so we implement it here to reduce boilerplate
// Feel free to move it down if some leaf signer is actually immediate
override suspend fun callExecutionType(): CallExecutionType {
return CallExecutionType.IMMEDIATE
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForSubmission(context: SigningContext) {
val accountId = metaAccount.requireAccountIdKeyIn(context.chain)
setNonce(context.getNonce(accountId))
Log.d("Signer", "${this::class.simpleName}: set real signature")
setVerifySignature(signer = this, accountId = accountId.value)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForFee(context: SigningContext) {
// We set it to 100 so we won't accidentally cause fee underestimation
// Underestimation might happen because fee depends on the extrinsic length and encoding of zero is more compact
setNonce(100.toBigInteger())
val (signer, accountId) = createFeeSigner(context.chain)
Log.d("Signer", "${this::class.simpleName}: set fake signature")
setVerifySignature(signer, accountId)
}
override suspend fun submissionSignerAccountId(chain: Chain): AccountId {
return metaAccount.requireAccountIdIn(chain)
}
context(ExtrinsicBuilder)
private fun createFeeSigner(chain: Chain): Pair<KeyPairSigner, AccountId> {
val keypair = generateFeeKeyPair(chain)
val signer = KeyPairSigner(keypair, feeMultiChainEncryption(chain))
return signer to chain.accountIdOf(keypair.publicKey)
}
private fun feeMultiChainEncryption(chain: Chain) = if (chain.isEthereumBased) {
MultiChainEncryption.Ethereum
} else {
MultiChainEncryption.Substrate(FAKE_CRYPTO_TYPE)
}
context(ExtrinsicBuilder)
private fun generateFeeKeyPair(chain: Chain): Keypair {
return if (chain.isEthereumBased) {
val emptySeed = ByteArray(64) { 1 }
Bip32EcdsaKeypairFactory.generate(emptySeed, junctions = emptyList())
} else {
val emptySeed = ByteArray(32) { 1 }
SubstrateKeypairFactory.generate(FAKE_CRYPTO_TYPE, emptySeed, junctions = emptyList())
}
}
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_account_impl.data.signer
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.ProxiedMetaAccount
import io.novafoundation.nova.feature_account_impl.data.signer.ledger.LedgerSignerFactory
import io.novafoundation.nova.feature_account_impl.data.signer.multisig.MultisigSignerFactory
import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignerFactory
import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxiedSignerFactory
import io.novafoundation.nova.feature_account_impl.data.signer.secrets.SecretsSignerFactory
import io.novafoundation.nova.feature_account_impl.data.signer.watchOnly.WatchOnlySignerFactory
internal class RealSignerProvider(
private val secretsSignerFactory: SecretsSignerFactory,
private val proxiedSignerFactory: ProxiedSignerFactory,
private val watchOnlySigner: WatchOnlySignerFactory,
private val polkadotVaultSignerFactory: PolkadotVaultVariantSignerFactory,
private val ledgerSignerFactory: LedgerSignerFactory,
private val multisigSignerFactory: MultisigSignerFactory
) : SignerProvider {
override fun rootSignerFor(metaAccount: MetaAccount): NovaSigner {
return signerFor(metaAccount, isRoot = true)
}
override fun nestedSignerFor(metaAccount: MetaAccount): NovaSigner {
return signerFor(metaAccount, isRoot = false)
}
private fun signerFor(metaAccount: MetaAccount, isRoot: Boolean): NovaSigner {
return when (metaAccount.type) {
LightMetaAccount.Type.SECRETS -> secretsSignerFactory.create(metaAccount)
LightMetaAccount.Type.WATCH_ONLY -> watchOnlySigner.create(metaAccount)
LightMetaAccount.Type.PARITY_SIGNER -> polkadotVaultSignerFactory.createParitySigner(metaAccount)
LightMetaAccount.Type.POLKADOT_VAULT -> polkadotVaultSignerFactory.createPolkadotVault(metaAccount)
LightMetaAccount.Type.LEDGER -> ledgerSignerFactory.create(metaAccount, LedgerVariant.GENERIC)
LightMetaAccount.Type.LEDGER_LEGACY -> ledgerSignerFactory.create(metaAccount, LedgerVariant.LEGACY)
LightMetaAccount.Type.PROXIED -> proxiedSignerFactory.create(metaAccount as ProxiedMetaAccount, this, isRoot)
LightMetaAccount.Type.MULTISIG -> multisigSignerFactory.create(metaAccount as MultisigMetaAccount, this, isRoot)
}
}
}
@@ -0,0 +1,66 @@
package io.novafoundation.nova.feature_account_impl.data.signer
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState
import io.novafoundation.nova.feature_account_api.data.signer.SignerPayload
import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenCommunicator
import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenRequester
import io.novafoundation.nova.feature_account_api.presenatation.sign.SignatureWrapper
import io.novafoundation.nova.feature_account_api.presenatation.sign.awaitConfirmation
import io.novafoundation.nova.runtime.extrinsic.signer.SignerPayloadRawWithChain
import io.novafoundation.nova.runtime.extrinsic.signer.withoutChain
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
abstract class SeparateFlowSigner(
private val signingSharedState: SigningSharedState,
private val signFlowRequester: SignInterScreenRequester,
metaAccount: MetaAccount,
) : LeafSigner(metaAccount) {
override suspend fun signInheritedImplication(
inheritedImplication: InheritedImplication,
accountId: AccountId
): SignatureWrapper {
val payload = SeparateFlowSignerState(SignerPayload.Extrinsic(inheritedImplication, accountId), metaAccount)
val result = awaitConfirmation(payload)
if (result is SignInterScreenCommunicator.Response.Signed) {
return SignatureWrapper(result.signature)
} else {
throw SigningCancelledException()
}
}
protected suspend fun useSignRawFlowRequester(payload: SignerPayloadRawWithChain): SignedRaw {
val state = SeparateFlowSignerState(SignerPayload.Raw(payload), metaAccount)
val result = awaitConfirmation(state)
if (result is SignInterScreenCommunicator.Response.Signed) {
val signature = SignatureWrapper(result.signature)
return SignedRaw(payload.withoutChain(), signature)
} else {
throw SigningCancelledException()
}
}
private suspend fun awaitConfirmation(state: SeparateFlowSignerState): SignInterScreenCommunicator.Response {
signingSharedState.set(state)
return withContext(Dispatchers.Main) {
try {
signFlowRequester.awaitConfirmation()
} finally {
signingSharedState.reset()
}
}
}
}
@@ -0,0 +1,76 @@
package io.novafoundation.nova.feature_account_impl.data.signer.ledger
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.presenatation.sign.LedgerSignCommunicator
import io.novafoundation.nova.feature_account_impl.R
import io.novafoundation.nova.feature_account_impl.data.signer.SeparateFlowSigner
import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import javax.inject.Inject
@FeatureScope
class LedgerSignerFactory @Inject constructor(
private val signingSharedState: SigningSharedState,
private val signFlowRequester: LedgerSignCommunicator,
private val resourceManager: ResourceManager,
private val messageSigningNotSupported: SigningNotSupportedPresentable,
) {
fun create(metaAccount: MetaAccount, ledgerVariant: LedgerVariant): LedgerSigner {
return LedgerSigner(
metaAccount = metaAccount,
signingSharedState = signingSharedState,
signFlowRequester = signFlowRequester,
resourceManager = resourceManager,
messageSigningNotSupported = messageSigningNotSupported,
ledgerVariant = ledgerVariant,
)
}
}
class LedgerSigner(
metaAccount: MetaAccount,
signingSharedState: SigningSharedState,
private val signFlowRequester: LedgerSignCommunicator,
private val ledgerVariant: LedgerVariant,
private val resourceManager: ResourceManager,
private val messageSigningNotSupported: SigningNotSupportedPresentable,
) : SeparateFlowSigner(signingSharedState, signFlowRequester, metaAccount) {
companion object {
// Ledger runs with quite severe resource restrictions so we should explicitly lower the number of calls per transaction
// Otherwise Ledger will run out of RAM when decoding such big txs
private const val MAX_CALLS_PER_TRANSACTION = 6
}
override suspend fun signInheritedImplication(inheritedImplication: InheritedImplication, accountId: AccountId): SignatureWrapper {
signFlowRequester.setUsedVariant(ledgerVariant)
return super.signInheritedImplication(inheritedImplication, accountId)
}
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
messageSigningNotSupported.presentSigningNotSupported(
SigningNotSupportedPresentable.Payload(
iconRes = R.drawable.ic_ledger,
message = resourceManager.getString(R.string.ledger_sign_raw_not_supported)
)
)
throw SigningCancelledException()
}
override suspend fun maxCallsPerTransaction(): Int {
return MAX_CALLS_PER_TRANSACTION
}
}
@@ -0,0 +1,203 @@
package io.novafoundation.nova.feature_account_impl.data.signer.multisig
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.data.memory.SingleValueCache
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_account_api.data.multisig.composeMultisigAsMulti
import io.novafoundation.nova.feature_account_api.data.multisig.composeMultisigAsMultiThreshold1
import io.novafoundation.nova.feature_account_api.data.multisig.validation.MultisigExtrinsicValidationPayload
import io.novafoundation.nova.feature_account_api.data.multisig.validation.MultisigExtrinsicValidationRequestBus
import io.novafoundation.nova.feature_account_api.data.multisig.validation.SignatoryFeePaymentMode
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
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.SubmissionHierarchy
import io.novafoundation.nova.feature_account_api.data.signer.intersect
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.isThreshold1
import io.novafoundation.nova.feature_account_impl.data.signer.LeafSigner
import io.novafoundation.nova.feature_account_impl.presentation.multisig.MultisigSigningPresenter
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 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.SignerPayloadRaw
import javax.inject.Inject
@FeatureScope
class MultisigSignerFactory @Inject constructor(
private val accountRepository: AccountRepository,
private val multisigExtrinsicValidationEventBus: MultisigExtrinsicValidationRequestBus,
private val multisigSigningPresenter: MultisigSigningPresenter,
) {
fun create(metaAccount: MultisigMetaAccount, signerProvider: SignerProvider, isRoot: Boolean): MultisigSigner {
return MultisigSigner(
accountRepository = accountRepository,
signerProvider = signerProvider,
isRootSigner = isRoot,
multisigExtrinsicValidationEventBus = multisigExtrinsicValidationEventBus,
multisigSigningPresenter = multisigSigningPresenter,
multisigAccount = metaAccount,
)
}
}
// TODO multisig:
// 1. Create a base class NestedSigner for Multisig and Proxieds
class MultisigSigner(
private val multisigAccount: MultisigMetaAccount,
private val accountRepository: AccountRepository,
private val signerProvider: SignerProvider,
private val multisigExtrinsicValidationEventBus: MultisigExtrinsicValidationRequestBus,
private val multisigSigningPresenter: MultisigSigningPresenter,
private val isRootSigner: Boolean,
) : NovaSigner {
override val metaAccount = multisigAccount
private val selfCallExecutionType = if (multisigAccount.isThreshold1()) {
CallExecutionType.IMMEDIATE
} else {
CallExecutionType.DELAYED
}
private val signatoryMetaAccount = SingleValueCache {
computeSignatoryMetaAccount()
}
private val delegateSigner = SingleValueCache {
signerProvider.nestedSignerFor(signatoryMetaAccount())
}
override suspend fun getSigningHierarchy(): SubmissionHierarchy {
return delegateSigner().getSigningHierarchy() + SubmissionHierarchy(metaAccount, selfCallExecutionType)
}
override suspend fun callExecutionType(): CallExecutionType {
return delegateSigner().callExecutionType().intersect(selfCallExecutionType)
}
override suspend fun submissionSignerAccountId(chain: Chain): AccountId {
return delegateSigner().submissionSignerAccountId(chain)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForSubmission(context: SigningContext) {
if (isRootSigner) {
acknowledgeMultisigOperation()
}
val callInsideAsMulti = getWrappedCall()
// We intentionally do validation before wrapping to pass the actual call to the validation
validateExtrinsic(context.chain, callInsideAsMulti)
wrapCallsInAsMultiForSubmission()
delegateSigner().setSignerDataForSubmission(context)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForFee(context: SigningContext) {
delegateSigner().setSignerDataForFee(context)
wrapCallsInProxyForFee()
}
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
multisigSigningPresenter.signingIsNotSupported()
throw SigningCancelledException()
}
override suspend fun maxCallsPerTransaction(): Int? {
return delegateSigner().maxCallsPerTransaction()
}
private suspend fun acknowledgeMultisigOperation() {
val resume = multisigSigningPresenter.acknowledgeMultisigOperation(multisigAccount, signatoryMetaAccount())
if (!resume) throw SigningCancelledException()
}
context(ExtrinsicBuilder)
private suspend fun validateExtrinsic(
chain: Chain,
callInsideAsMulti: GenericCall.Instance,
) {
val validationPayload = MultisigExtrinsicValidationPayload(
multisig = multisigAccount,
signatory = signatoryMetaAccount(),
chain = chain,
signatoryFeePaymentMode = determineSignatoryFeePaymentMode(),
callInsideAsMulti = callInsideAsMulti
)
val requestBusPayload = MultisigExtrinsicValidationRequestBus.Request(validationPayload)
multisigExtrinsicValidationEventBus.handle(requestBusPayload)
.validationResult
.onSuccess {
if (it is ValidationStatus.NotValid) {
multisigSigningPresenter.presentValidationFailure(it.reason)
throw SigningCancelledException()
}
}
.onFailure {
throw it
}
}
context(ExtrinsicBuilder)
private suspend fun determineSignatoryFeePaymentMode(): SignatoryFeePaymentMode {
// Our direct signatory only pay fees if it is a LeafSigner
// Otherwise it is paid by signer's own delegate
return if (delegateSigner() is LeafSigner) {
SignatoryFeePaymentMode.PaysSubmissionFee
} else {
SignatoryFeePaymentMode.NothingToPay
}
}
context(ExtrinsicBuilder)
private fun wrapCallsInAsMultiForSubmission() {
// We do not calculate precise max_weight as it is only needed for the final approval
return wrapCallsInAsMulti(maxWeight = WeightV2.zero())
}
context(ExtrinsicBuilder)
private fun wrapCallsInProxyForFee() {
wrapCallsInAsMulti(maxWeight = WeightV2.zero())
}
context(ExtrinsicBuilder)
private fun wrapCallsInAsMulti(maxWeight: WeightV2) {
val call = getWrappedCall()
val multisigCall = if (multisigAccount.isThreshold1()) {
runtime.composeMultisigAsMultiThreshold1(
multisigMetaAccount = multisigAccount,
call = call
)
} else {
runtime.composeMultisigAsMulti(
multisigMetaAccount = multisigAccount,
maybeTimePoint = null,
call = call,
maxWeight = maxWeight
)
}
resetCalls()
call(multisigCall)
}
private suspend fun computeSignatoryMetaAccount(): MetaAccount {
return accountRepository.getMetaAccount(multisigAccount.signatoryMetaId)
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner
import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant
import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenCommunicator
interface PolkadotVaultVariantSignCommunicator : SignInterScreenCommunicator {
fun setUsedVariant(variant: PolkadotVaultVariant)
}
@@ -0,0 +1,137 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant
import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider
import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.formatWithPolkadotVaultLabel
import io.novafoundation.nova.feature_account_impl.R
import io.novafoundation.nova.feature_account_impl.data.signer.SeparateFlowSigner
import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novafoundation.nova.runtime.extrinsic.signer.SignerPayloadRawWithChain
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import javax.inject.Inject
@FeatureScope
class PolkadotVaultVariantSignerFactory @Inject constructor(
private val signingSharedState: SigningSharedState,
private val signFlowRequester: PolkadotVaultVariantSignCommunicator,
private val resourceManager: ResourceManager,
private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider,
private val messageSigningNotSupported: SigningNotSupportedPresentable,
) {
fun createPolkadotVault(metaAccount: MetaAccount): PolkadotVaultSigner {
return PolkadotVaultSigner(
signingSharedState = signingSharedState,
metaAccount = metaAccount,
signFlowRequester = signFlowRequester,
resourceManager = resourceManager,
polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider,
messageSigningNotSupported = messageSigningNotSupported,
)
}
fun createParitySigner(metaAccount: MetaAccount): ParitySignerSigner {
return ParitySignerSigner(
signingSharedState = signingSharedState,
metaAccount = metaAccount,
signFlowRequester = signFlowRequester,
resourceManager = resourceManager,
polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider,
messageSigningNotSupported = messageSigningNotSupported,
)
}
}
abstract class PolkadotVaultVariantSigner(
signingSharedState: SigningSharedState,
metaAccount: MetaAccount,
private val signFlowRequester: PolkadotVaultVariantSignCommunicator,
private val resourceManager: ResourceManager,
private val variant: PolkadotVaultVariant,
private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider,
private val messageSigningNotSupported: SigningNotSupportedPresentable,
) : SeparateFlowSigner(signingSharedState, signFlowRequester, metaAccount) {
override suspend fun signInheritedImplication(inheritedImplication: InheritedImplication, accountId: AccountId): SignatureWrapper {
signFlowRequester.setUsedVariant(variant)
return super.signInheritedImplication(inheritedImplication, accountId)
}
// Vault does not support chain-less message signing yet
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
rawSigningNotSupported()
}
protected suspend fun rawSigningNotSupported(): Nothing {
val config = polkadotVaultVariantConfigProvider.variantConfigFor(variant)
messageSigningNotSupported.presentSigningNotSupported(
SigningNotSupportedPresentable.Payload(
iconRes = config.common.iconRes,
message = resourceManager.formatWithPolkadotVaultLabel(R.string.account_parity_signer_not_supported_subtitle, variant)
)
)
throw SigningCancelledException()
}
override suspend fun maxCallsPerTransaction(): Int? {
return null
}
}
class ParitySignerSigner(
signingSharedState: SigningSharedState,
metaAccount: MetaAccount,
signFlowRequester: PolkadotVaultVariantSignCommunicator,
resourceManager: ResourceManager,
polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider,
messageSigningNotSupported: SigningNotSupportedPresentable,
) : PolkadotVaultVariantSigner(
signingSharedState = signingSharedState,
metaAccount = metaAccount,
signFlowRequester = signFlowRequester,
resourceManager = resourceManager,
variant = PolkadotVaultVariant.PARITY_SIGNER,
polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider,
messageSigningNotSupported = messageSigningNotSupported,
) {
override suspend fun signRawWithChain(payload: SignerPayloadRawWithChain): SignedRaw {
rawSigningNotSupported()
}
}
class PolkadotVaultSigner(
signingSharedState: SigningSharedState,
metaAccount: MetaAccount,
private val signFlowRequester: PolkadotVaultVariantSignCommunicator,
resourceManager: ResourceManager,
polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider,
messageSigningNotSupported: SigningNotSupportedPresentable,
) : PolkadotVaultVariantSigner(
signingSharedState = signingSharedState,
metaAccount = metaAccount,
signFlowRequester = signFlowRequester,
resourceManager = resourceManager,
variant = PolkadotVaultVariant.POLKADOT_VAULT,
polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider,
messageSigningNotSupported = messageSigningNotSupported,
) {
override suspend fun signRawWithChain(payload: SignerPayloadRawWithChain): SignedRaw {
signFlowRequester.setUsedVariant(PolkadotVaultVariant.POLKADOT_VAULT)
return useSignRawFlowRequester(payload)
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.multiFrame
import io.novafoundation.nova.common.utils.toByteArray
import java.nio.ByteOrder
object LegacyMultiPart {
private val MULTI_FRAME_TYPE: ByteArray = byteArrayOf(0x00)
fun createSingle(payload: ByteArray): ByteArray {
val frameCount: Short = 1
val frameIndex: Short = 0
return MULTI_FRAME_TYPE +
frameCount.encodeMultiPartNumber() +
frameIndex.encodeMultiPartNumber() +
payload
}
fun createMultiple(payloads: List<ByteArray>): List<ByteArray> {
val frameCount = payloads.size
val prefix = MULTI_FRAME_TYPE + frameCount.encodeMultiPartNumber()
return payloads.mapIndexed { index, payload ->
prefix + index.encodeMultiPartNumber() + payload
}
}
private fun Number.encodeMultiPartNumber(): ByteArray = toShort().toByteArray(ByteOrder.BIG_ENDIAN)
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.transaction
import io.novafoundation.nova.feature_account_api.data.signer.SignerPayload
import io.novafoundation.nova.runtime.extrinsic.metadata.ExtrinsicProof
import io.novafoundation.nova.runtime.extrinsic.signer.SignerPayloadRawWithChain
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.getGenesisHashOrThrow
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.transientEncodedCallData
fun SignerPayload.Extrinsic.paritySignerLegacyTxPayload(): ByteArray {
return accountId + extrinsic.transientEncodedCallData() + extrinsic.encodedExtensions() + extrinsic.getGenesisHashOrThrow()
}
fun SignerPayload.Extrinsic.paritySignerTxPayloadWithProof(proof: ExtrinsicProof): ByteArray {
return accountId + proof.value + extrinsic.transientEncodedCallData() + extrinsic.encodedExtensions() + extrinsic.getGenesisHashOrThrow()
}
fun SignerPayloadRawWithChain.polkadotVaultSignRawPayload(): ByteArray {
return accountId + message + chainId.fromHex()
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.uos
import io.novafoundation.nova.core.model.CryptoType
enum class ParitySignerUOSContentCode(override val value: Byte) : UOS.UOSPreludeValue {
SUBSTRATE(0x53),
}
enum class ParitySignerUOSPayloadCode(override val value: Byte) : UOS.UOSPreludeValue {
TRANSACTION(0x02), MESSAGE(0x03), TRANSACTION_WITH_PROOF(0x06)
}
fun CryptoType.paritySignerUOSCryptoType(): UOS.UOSPreludeValue {
val byte: Byte = when (this) {
CryptoType.ED25519 -> 0x00
CryptoType.SR25519 -> 0x01
CryptoType.ECDSA -> 0x02
}
return SimpleUOSPreludeValue(byte)
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.uos
object UOS {
interface UOSPreludeValue {
val value: Byte
}
fun createUOSPayload(
payload: ByteArray,
contentCode: UOSPreludeValue,
cryptoCode: UOSPreludeValue,
payloadCode: UOSPreludeValue
): ByteArray {
return byteArrayOf(contentCode.value, cryptoCode.value, payloadCode.value) + payload
}
}
class SimpleUOSPreludeValue(override val value: Byte) : UOS.UOSPreludeValue
@@ -0,0 +1,244 @@
package io.novafoundation.nova.feature_account_impl.data.signer.proxy
import android.util.Log
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.data.memory.SingleValueCache
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.common.utils.composeCall
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxiedExtrinsicValidationFailure.ProxyNotEnoughFee
import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxiedExtrinsicValidationPayload
import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
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.SubmissionHierarchy
import io.novafoundation.nova.feature_account_api.data.signer.intersect
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.ProxiedMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.feature_account_api.presenatation.account.proxy.ProxySigningPresenter
import io.novafoundation.nova.feature_account_impl.data.signer.LeafSigner
import io.novafoundation.nova.feature_proxy_api.data.repository.GetProxyRepository
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novafoundation.nova.runtime.ext.commissionAsset
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.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novafoundation.nova.common.utils.PezkuwiAddressConstructor
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.SignerPayloadRaw
import javax.inject.Inject
@FeatureScope
class ProxiedSignerFactory @Inject constructor(
private val accountRepository: AccountRepository,
private val proxySigningPresenter: ProxySigningPresenter,
private val getProxyRepository: GetProxyRepository,
private val proxyExtrinsicValidationEventBus: ProxyExtrinsicValidationRequestBus,
private val proxyCallFilterFactory: ProxyCallFilterFactory
) {
fun create(metaAccount: ProxiedMetaAccount, signerProvider: SignerProvider, isRoot: Boolean): ProxiedSigner {
return ProxiedSigner(
accountRepository = accountRepository,
signerProvider = signerProvider,
proxySigningPresenter = proxySigningPresenter,
getProxyRepository = getProxyRepository,
proxyExtrinsicValidationEventBus = proxyExtrinsicValidationEventBus,
isRootSigner = isRoot,
proxyCallFilterFactory = proxyCallFilterFactory,
proxiedMetaAccount = metaAccount
)
}
}
class ProxiedSigner(
private val proxiedMetaAccount: ProxiedMetaAccount,
private val accountRepository: AccountRepository,
private val signerProvider: SignerProvider,
private val proxySigningPresenter: ProxySigningPresenter,
private val getProxyRepository: GetProxyRepository,
private val proxyExtrinsicValidationEventBus: ProxyExtrinsicValidationRequestBus,
private val isRootSigner: Boolean,
private val proxyCallFilterFactory: ProxyCallFilterFactory,
) : NovaSigner {
override val metaAccount = proxiedMetaAccount
private val selfCallExecutionType = CallExecutionType.IMMEDIATE
private val proxyMetaAccount = SingleValueCache {
computeProxyMetaAccount()
}
private val delegateSigner = SingleValueCache {
signerProvider.nestedSignerFor(proxyMetaAccount())
}
override suspend fun getSigningHierarchy(): SubmissionHierarchy {
return delegateSigner().getSigningHierarchy() + SubmissionHierarchy(metaAccount, selfCallExecutionType)
}
override suspend fun submissionSignerAccountId(chain: Chain): AccountId {
return delegateSigner().submissionSignerAccountId(chain)
}
override suspend fun callExecutionType(): CallExecutionType {
return delegateSigner().callExecutionType().intersect(selfCallExecutionType)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForSubmission(context: SigningContext) {
if (isRootSigner) {
acknowledgeProxyOperation(proxyMetaAccount())
}
val proxiedCall = getWrappedCall()
validateExtrinsic(context.chain, proxyMetaAccount = proxyMetaAccount(), proxiedCall = proxiedCall)
wrapCallsInProxyForSubmission(context.chain, proxiedCall = proxiedCall)
Log.d("Signer", "ProxiedSigner: wrapped proxy calls for submission")
delegateSigner().setSignerDataForSubmission(context)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForFee(context: SigningContext) {
val proxiedCall = getWrappedCall()
wrapCallsInProxyForFee(context.chain, proxiedCall = proxiedCall)
Log.d("Signer", "ProxiedSigner: wrapped proxy calls for fee")
delegateSigner().setSignerDataForFee(context)
}
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
signingNotSupported()
}
override suspend fun maxCallsPerTransaction(): Int? {
return delegateSigner().maxCallsPerTransaction()
}
context(ExtrinsicBuilder)
private suspend fun validateExtrinsic(
chain: Chain,
proxyMetaAccount: MetaAccount,
proxiedCall: GenericCall.Instance,
) {
if (!proxyPaysFees()) return
val validationPayload = ProxiedExtrinsicValidationPayload(
proxiedMetaAccount = proxiedMetaAccount,
proxyMetaAccount = proxyMetaAccount,
chainWithAsset = ChainWithAsset(chain, chain.commissionAsset),
proxiedCall = proxiedCall
)
val requestBusPayload = ProxyExtrinsicValidationRequestBus.Request(validationPayload)
proxyExtrinsicValidationEventBus.handle(requestBusPayload)
.validationResult
.onSuccess {
if (it is ValidationStatus.NotValid && it.reason is ProxyNotEnoughFee) {
val reason = it.reason as ProxyNotEnoughFee
proxySigningPresenter.notEnoughFee(reason.proxy, reason.asset, reason.availableBalance, reason.fee)
throw SigningCancelledException()
}
}
.onFailure {
throw it
}
}
context(ExtrinsicBuilder)
private suspend fun wrapCallsInProxyForSubmission(chain: Chain, proxiedCall: GenericCall.Instance) {
val proxyAccountId = proxyMetaAccount().requireAccountIdIn(chain)
val proxiedAccountId = proxiedMetaAccount.requireAccountIdIn(chain)
val availableProxyTypes = getProxyRepository.getDelegatedProxyTypesRemote(
chainId = chain.id,
proxiedAccountId = proxiedAccountId,
proxyAccountId = proxyAccountId
)
val proxyType = proxyCallFilterFactory.getFirstMatchedTypeOrNull(proxiedCall, availableProxyTypes)
?: notEnoughPermission(proxyMetaAccount(), availableProxyTypes)
return wrapCallsIntoProxy(
proxiedAccountId = proxiedAccountId,
proxyType = proxyType,
proxiedCall = proxiedCall
)
}
// Wrap without verifying proxy permissions and hardcode proxy type
// to speed up fee calculation
context(ExtrinsicBuilder)
private fun wrapCallsInProxyForFee(chain: Chain, proxiedCall: GenericCall.Instance) {
val proxiedAccountId = proxiedMetaAccount.requireAccountIdIn(chain)
return wrapCallsIntoProxy(
proxiedAccountId = proxiedAccountId,
proxyType = ProxyType.Any,
proxiedCall = proxiedCall
)
}
context(ExtrinsicBuilder)
private fun wrapCallsIntoProxy(
proxiedAccountId: AccountId,
proxyType: ProxyType,
proxiedCall: GenericCall.Instance,
) {
val proxyCall = runtime.composeCall(
moduleName = Modules.PROXY,
callName = "proxy",
arguments = mapOf(
"real" to PezkuwiAddressConstructor.constructInstance(runtime.typeRegistry, proxiedAccountId),
"force_proxy_type" to DictEnum.Entry(proxyType.name, null),
"call" to proxiedCall
)
)
resetCalls()
call(proxyCall)
}
private suspend fun acknowledgeProxyOperation(proxyMetaAccount: MetaAccount) {
val resume = proxySigningPresenter.acknowledgeProxyOperation(proxiedMetaAccount, proxyMetaAccount)
if (!resume) {
throw SigningCancelledException()
}
}
private suspend fun computeProxyMetaAccount(): MetaAccount {
val proxyAccount = proxiedMetaAccount.proxy
return accountRepository.getMetaAccount(proxyAccount.proxyMetaId)
}
private suspend fun notEnoughPermission(proxyMetaAccount: MetaAccount, availableProxyTypes: List<ProxyType>): Nothing {
proxySigningPresenter.notEnoughPermission(proxiedMetaAccount, proxyMetaAccount, availableProxyTypes)
throw SigningCancelledException()
}
private suspend fun signingNotSupported(): Nothing {
proxySigningPresenter.signingIsNotSupported()
throw SigningCancelledException()
}
private suspend fun proxyPaysFees(): Boolean {
// Our direct proxy only pay fees it is a leaf. Otherwise fees paid by proxy's own delegate
return delegateSigner() is LeafSigner
}
}
@@ -0,0 +1,95 @@
package io.novafoundation.nova.feature_account_impl.data.signer.proxy
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.CallFilter
import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.AnyOfCallFilter
import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.EverythingFilter
import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.WhiteListFilter
import io.novafoundation.nova.feature_proxy_api.domain.model.ProxyType
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
class ProxyCallFilterFactory {
fun getCallFilterFor(proxyType: ProxyType): CallFilter {
return when (proxyType) {
ProxyType.Any,
is ProxyType.Other -> EverythingFilter()
ProxyType.NonTransfer -> AnyOfCallFilter(
WhiteListFilter(Modules.SYSTEM),
WhiteListFilter(Modules.SCHEDULER),
WhiteListFilter(Modules.BABE),
WhiteListFilter(Modules.TIMESTAMP),
WhiteListFilter(Modules.INDICES, listOf("claim", "free", "freeze")),
WhiteListFilter(Modules.STAKING),
WhiteListFilter(Modules.SESSION),
WhiteListFilter(Modules.GRANDPA),
WhiteListFilter(Modules.IM_ONLINE),
WhiteListFilter(Modules.TREASURY),
WhiteListFilter(Modules.BOUNTIES),
WhiteListFilter(Modules.CHILD_BOUNTIES),
WhiteListFilter(Modules.CONVICTION_VOTING),
WhiteListFilter(Modules.REFERENDA),
WhiteListFilter(Modules.WHITELIST),
WhiteListFilter(Modules.CLAIMS),
WhiteListFilter(Modules.VESTING, listOf("vest", "vest_other")),
WhiteListFilter(Modules.UTILITY),
WhiteListFilter(Modules.IDENTITY),
WhiteListFilter(Modules.PROXY),
WhiteListFilter(Modules.MULTISIG),
WhiteListFilter(Modules.REGISTRAR, listOf("register", "deregister", "reserve")),
WhiteListFilter(Modules.CROWDLOAN),
WhiteListFilter(Modules.SLOTS),
WhiteListFilter(Modules.AUCTIONS),
WhiteListFilter(Modules.VOTER_LIST),
WhiteListFilter(Modules.NOMINATION_POOLS),
WhiteListFilter(Modules.FAST_UNSTAKE)
)
ProxyType.Governance -> AnyOfCallFilter(
WhiteListFilter(Modules.TREASURY),
WhiteListFilter(Modules.BOUNTIES),
WhiteListFilter(Modules.UTILITY),
WhiteListFilter(Modules.CHILD_BOUNTIES),
WhiteListFilter(Modules.CONVICTION_VOTING),
WhiteListFilter(Modules.REFERENDA),
WhiteListFilter(Modules.WHITELIST)
)
ProxyType.Staking -> AnyOfCallFilter(
WhiteListFilter(Modules.STAKING),
WhiteListFilter(Modules.SESSION),
WhiteListFilter(Modules.UTILITY),
WhiteListFilter(Modules.FAST_UNSTAKE),
WhiteListFilter(Modules.VOTER_LIST),
WhiteListFilter(Modules.NOMINATION_POOLS)
)
ProxyType.NominationPools -> AnyOfCallFilter(
WhiteListFilter(Modules.NOMINATION_POOLS),
WhiteListFilter(Modules.UTILITY)
)
ProxyType.IdentityJudgement -> AnyOfCallFilter(
WhiteListFilter(Modules.IDENTITY, listOf("provide_judgement")),
WhiteListFilter(Modules.UTILITY)
)
ProxyType.CancelProxy -> WhiteListFilter(Modules.PROXY, listOf("reject_announcement"))
ProxyType.Auction -> AnyOfCallFilter(
WhiteListFilter(Modules.AUCTIONS),
WhiteListFilter(Modules.CROWDLOAN),
WhiteListFilter(Modules.REGISTRAR),
WhiteListFilter(Modules.SLOTS)
)
}
}
}
fun ProxyCallFilterFactory.getFirstMatchedTypeOrNull(call: GenericCall.Instance, proxyTypes: List<ProxyType>): ProxyType? {
return proxyTypes.firstOrNull {
val callFilter = this.getCallFilterFor(it)
callFilter.canExecute(call)
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
class AnyOfCallFilter(
private val filters: List<CallFilter>
) : CallFilter {
constructor(vararg filters: CallFilter) : this(filters.toList())
override fun canExecute(call: GenericCall.Instance): Boolean {
return filters.any { it.canExecute(call) }
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
interface CallFilter {
fun canExecute(call: GenericCall.Instance): Boolean
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
class EverythingFilter : CallFilter {
override fun canExecute(call: GenericCall.Instance): Boolean {
return true
}
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
class WhiteListFilter(private val matchingModule: String, private val matchingCalls: List<String>?) : CallFilter {
constructor(matchingModule: String) : this(matchingModule, null)
override fun canExecute(call: GenericCall.Instance): Boolean {
val callModule = call.module.name
val callName = call.function.name
if (matchingModule == callModule) {
if (matchingCalls == null) return true
if (matchingCalls.contains(callName)) return true
}
return false
}
}
@@ -0,0 +1,189 @@
package io.novafoundation.nova.feature_account_impl.data.signer.secrets
import io.novafoundation.nova.common.base.errors.SigningCancelledException
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.getChainAccountKeypair
import io.novafoundation.nova.common.data.secrets.v2.getMetaAccountKeypair
import io.novafoundation.nova.common.data.secrets.v2.seed
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationResult
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.ethereumAccountId
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
import io.novafoundation.nova.feature_account_api.domain.model.substrateFrom
import io.novafoundation.nova.feature_account_impl.data.signer.LeafSigner
import io.novafoundation.nova.runtime.ext.isPezkuwiChain
import io.novafoundation.nova.runtime.extrinsic.signer.PezkuwiKeyPairSigner
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.ChainsById
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chainsById
import io.novasama.substrate_sdk_android.encrypt.MultiChainEncryption
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
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.KeyPairSigner
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckNonce.Companion.setNonce
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.verifySignature.VerifySignature.Companion.setVerifySignature
import javax.inject.Inject
@FeatureScope
class SecretsSignerFactory @Inject constructor(
private val secretStoreV2: SecretStoreV2,
private val chainRegistry: ChainRegistry,
private val twoFactorVerificationService: TwoFactorVerificationService
) {
fun create(metaAccount: MetaAccount): SecretsSigner {
return SecretsSigner(
metaAccount = metaAccount,
secretStoreV2 = secretStoreV2,
chainRegistry = chainRegistry,
twoFactorVerificationService = twoFactorVerificationService
)
}
}
class SecretsSigner(
metaAccount: MetaAccount,
private val secretStoreV2: SecretStoreV2,
private val chainRegistry: ChainRegistry,
private val twoFactorVerificationService: TwoFactorVerificationService,
) : LeafSigner(metaAccount) {
// Track current signing chain to determine which context to use
@Volatile
private var currentSigningChain: Chain? = null
context(ExtrinsicBuilder)
override suspend fun setSignerDataForSubmission(context: SigningContext) {
// Capture the chain for use in signInheritedImplication
currentSigningChain = context.chain
val accountId = metaAccount.requireAccountIdKeyIn(context.chain)
setNonce(context.getNonce(accountId))
setVerifySignature(signer = this, accountId = accountId.value)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForFee(context: SigningContext) {
// Capture the chain for use in signInheritedImplication
currentSigningChain = context.chain
// Call parent implementation for fee signing
super.setSignerDataForFee(context)
}
override suspend fun signInheritedImplication(
inheritedImplication: InheritedImplication,
accountId: AccountId
): SignatureWrapper {
runTwoFactorVerificationIfEnabled()
val chain = currentSigningChain
val keypair = getKeypair(accountId)
// Use PezkuwiKeyPairSigner for Pezkuwi chains (bizinikiwi context)
// Use standard KeyPairSigner for other chains (substrate context)
return if (chain?.isPezkuwiChain == true) {
// Get the original seed for Pezkuwi signing
val seed = getSeed(accountId) ?: error("No seed found for Pezkuwi signing")
val pezkuwiSigner = PezkuwiKeyPairSigner.fromSeed(seed)
pezkuwiSigner.signInheritedImplication(inheritedImplication, accountId)
} else {
val delegate = createDelegate(accountId, keypair)
delegate.signInheritedImplication(inheritedImplication, accountId)
}
}
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
runTwoFactorVerificationIfEnabled()
val chain = currentSigningChain
val keypair = getKeypair(payload.accountId)
// Use PezkuwiKeyPairSigner for Pezkuwi chains (bizinikiwi context)
return if (chain?.isPezkuwiChain == true) {
// Get the original seed for Pezkuwi signing
val seed = getSeed(payload.accountId) ?: error("No seed found for Pezkuwi signing")
val pezkuwiSigner = PezkuwiKeyPairSigner.fromSeed(seed)
pezkuwiSigner.signRaw(payload)
} else {
val delegate = createDelegate(payload.accountId, keypair)
delegate.signRaw(payload)
}
}
private suspend fun getSeed(accountId: AccountId): ByteArray? {
val secrets = secretStoreV2.getAccountSecrets(metaAccount.id, accountId)
return secrets.seed()
}
override suspend fun maxCallsPerTransaction(): Int? {
return null
}
private suspend fun runTwoFactorVerificationIfEnabled() {
if (twoFactorVerificationService.isEnabled()) {
val confirmationResult = twoFactorVerificationService.requestConfirmationIfEnabled()
if (confirmationResult != TwoFactorVerificationResult.CONFIRMED) {
throw SigningCancelledException()
}
}
}
private suspend fun getKeypair(accountId: AccountId): Keypair {
val chainsById = chainRegistry.chainsById()
val multiChainEncryption = metaAccount.multiChainEncryptionFor(accountId, chainsById)!!
return secretStoreV2.getKeypair(
metaAccount = metaAccount,
accountId = accountId,
isEthereumBased = multiChainEncryption is MultiChainEncryption.Ethereum
)
}
private suspend fun createDelegate(accountId: AccountId, keypair: Keypair): KeyPairSigner {
val chainsById = chainRegistry.chainsById()
val multiChainEncryption = metaAccount.multiChainEncryptionFor(accountId, chainsById)!!
return KeyPairSigner(keypair, multiChainEncryption)
}
private suspend fun SecretStoreV2.getKeypair(
metaAccount: MetaAccount,
accountId: AccountId,
isEthereumBased: Boolean
) = if (hasChainSecrets(metaAccount.id, accountId)) {
getChainAccountKeypair(metaAccount.id, accountId)
} else {
getMetaAccountKeypair(metaAccount.id, isEthereumBased)
}
/**
@return [MultiChainEncryption] for given [accountId] inside this meta account or null in case it was not possible to determine result
*/
private fun MetaAccount.multiChainEncryptionFor(accountId: ByteArray, chainsById: ChainsById): MultiChainEncryption? {
return when {
substrateAccountId.contentEquals(accountId) -> substrateCryptoType?.let(MultiChainEncryption.Companion::substrateFrom)
ethereumAccountId().contentEquals(accountId) -> MultiChainEncryption.Ethereum
else -> {
val chainAccount = chainAccounts.values.firstOrNull { it.accountId.contentEquals(accountId) } ?: return null
val cryptoType = chainAccount.cryptoType ?: return null
val chain = chainsById[chainAccount.chainId] ?: return null
if (chain.isEthereumBased) {
MultiChainEncryption.Ethereum
} else {
MultiChainEncryption.substrateFrom(cryptoType)
}
}
}
}
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_account_impl.data.signer.signingContext
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
import io.novasama.substrate_sdk_android.runtime.extrinsic.Nonce
class DefaultSigningContext(
override val chain: Chain,
private val rpcCalls: RpcCalls,
) : SigningContext {
override suspend fun getNonce(accountId: AccountIdKey): Nonce {
return rpcCalls.getNonce(chain.id, chain.addressOf(accountId))
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_account_impl.data.signer.signingContext
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novasama.substrate_sdk_android.runtime.extrinsic.Nonce
import java.math.BigInteger
class SequenceSigningContext(
private val delegate: SigningContext
) : SigningContext by delegate {
private var offset: BigInteger = BigInteger.ZERO
fun incrementNonceOffset() {
offset += BigInteger.ONE
}
override suspend fun getNonce(accountId: AccountIdKey): Nonce {
val delegateNonce = delegate.getNonce(accountId)
return delegateNonce + offset
}
}
fun SigningContext.withSequenceSigning(): SequenceSigningContext {
return SequenceSigningContext(this)
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_account_impl.data.signer.signingContext
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
import javax.inject.Inject
@FeatureScope
internal class SigningContextFactory @Inject constructor(
private val rpcCalls: RpcCalls
) : SigningContext.Factory {
override fun default(chain: Chain): SigningContext {
return DefaultSigningContext(chain, rpcCalls)
}
}
@@ -0,0 +1,50 @@
package io.novafoundation.nova.feature_account_impl.data.signer.watchOnly
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter
import io.novafoundation.nova.feature_account_impl.data.signer.LeafSigner
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import javax.inject.Inject
@FeatureScope
class WatchOnlySignerFactory @Inject constructor(
private val watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter,
) {
fun create(metaAccount: MetaAccount): WatchOnlySigner {
return WatchOnlySigner(watchOnlySigningPresenter, metaAccount)
}
}
class WatchOnlySigner(
private val watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter,
metaAccount: MetaAccount
) : LeafSigner(metaAccount) {
override suspend fun signInheritedImplication(
inheritedImplication: InheritedImplication,
accountId: AccountId
): SignatureWrapper {
cannotSign()
}
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
cannotSign()
}
override suspend fun maxCallsPerTransaction(): Int? {
return null
}
private suspend fun cannotSign(): Nothing {
watchOnlySigningPresenter.presentNoKeysFound()
throw SigningCancelledException()
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_account_impl.data.sync
import android.util.Log
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.flatMapAsync
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.extensions.tryFindNonNull
internal class CompoundExternalAccountsSyncDataSource(
private val delegates: List<ExternalAccountsSyncDataSource>
) : ExternalAccountsSyncDataSource {
override fun supportedChains(): Collection<Chain> {
return delegates.flatMap { it.supportedChains() }
.distinctBy { it.id }
}
override suspend fun isCreatedFromDataSource(metaAccount: MetaAccount): Boolean {
return delegates.any { it.isCreatedFromDataSource(metaAccount) }
}
override suspend fun getExternalCreatedAccount(metaAccount: MetaAccount): ExternalSourceCreatedAccount? {
return delegates.tryFindNonNull { it.getExternalCreatedAccount(metaAccount) }
}
override suspend fun getControllableExternalAccounts(
accountIdsToQuery: Set<AccountIdKey>,
): List<ExternalControllableAccount> {
return delegates.flatMapAsync {
val label = it::class.simpleName
Log.d("ExternalAccountsDiscovery", "Started fetching ${accountIdsToQuery.size} accounts using $label")
try {
it.getControllableExternalAccounts(accountIdsToQuery).also { result ->
Log.d("ExternalAccountsDiscovery", "Finished fetching accounts using $label. Got ${result.size} accounts")
}
} catch (e: Throwable) {
Log.e("ExternalAccountsDiscovery", "Failed to fetch accounts using $label", e)
throw e
}
}
}
}
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_account_impl.data.sync
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
internal interface ExternalAccountsSyncDataSource {
interface Factory {
suspend fun create(): ExternalAccountsSyncDataSource
}
fun supportedChains(): Collection<Chain>
suspend fun isCreatedFromDataSource(metaAccount: MetaAccount): Boolean
suspend fun getExternalCreatedAccount(metaAccount: MetaAccount): ExternalSourceCreatedAccount?
suspend fun getControllableExternalAccounts(accountIdsToQuery: Set<AccountIdKey>): List<ExternalControllableAccount>
}
internal interface ExternalControllableAccount {
val accountId: AccountIdKey
val controllerAccountId: AccountIdKey
/**
* Check whether [localAccount] represents self in the data-base
* Implementation can assume that [accountId] and [controllerAccountId] check has already been done
*/
fun isRepresentedBy(localAccount: MetaAccount): Boolean
fun isAvailableOn(chain: Chain): Boolean
/**
* Add account to the data-base, WITHOUT notifying any external entities,
* like [MetaAccountChangesEventBus] - this is expected to be done by the calling code
*
* @return id of newly created account
*/
suspend fun addControlledAccount(
controller: MetaAccount,
identity: Identity?,
position: Int,
missingAccountChain: Chain
): AddAccountResult.AccountAdded
/**
* Whether dispatching call on behalf of this account changes the original call filters
*
* This might be used by certain data-sources to understand whether control of such account is actually possible
*/
fun dispatchChangesOriginFilters(): Boolean
}
internal interface ExternalSourceCreatedAccount {
fun canControl(candidate: ExternalControllableAccount): Boolean
}
internal fun ExternalControllableAccount.address(chain: Chain): String {
return chain.addressOf(accountId)
}
internal fun ExternalControllableAccount.controllerAddress(chain: Chain): String {
return chain.addressOf(controllerAccountId)
}
@@ -0,0 +1,264 @@
package io.novafoundation.nova.feature_account_impl.data.sync
import com.google.gson.Gson
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.format.AddressFormat
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.address.format.addressOf
import io.novafoundation.nova.common.address.format.getAddressScheme
import io.novafoundation.nova.common.address.format.isEvm
import io.novafoundation.nova.common.address.format.isSubstrate
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.core_db.model.chain.account.MultisigTypeExtras
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountResult
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
import io.novafoundation.nova.feature_account_impl.data.multisig.MultisigRepository
import io.novafoundation.nova.feature_account_impl.data.multisig.model.otherSignatories
import io.novafoundation.nova.runtime.ext.addressScheme
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.findChains
import javax.inject.Inject
@FeatureScope
internal class MultisigAccountsSyncDataSourceFactory @Inject constructor(
private val multisigRepository: MultisigRepository,
private val gson: Gson,
private val accountDao: MetaAccountDao,
private val chainRegistry: ChainRegistry
) : ExternalAccountsSyncDataSource.Factory {
override suspend fun create(): ExternalAccountsSyncDataSource {
val chainsWithMultisigs = chainRegistry.findChains(multisigRepository::supportsMultisigSync)
return MultisigAccountsSyncDataSource(multisigRepository, gson, accountDao, chainsWithMultisigs)
}
}
private class MultisigAccountsSyncDataSource(
private val multisigRepository: MultisigRepository,
private val gson: Gson,
private val accountDao: MetaAccountDao,
private val multisigChains: List<Chain>,
) : ExternalAccountsSyncDataSource {
private val multisigChainIds = multisigChains.mapToSet { it.id }
override fun supportedChains(): Collection<Chain> {
return multisigChains
}
override suspend fun isCreatedFromDataSource(metaAccount: MetaAccount): Boolean {
return metaAccount is MultisigMetaAccount
}
override suspend fun getExternalCreatedAccount(metaAccount: MetaAccount): ExternalSourceCreatedAccount? {
return if (isCreatedFromDataSource(metaAccount)) {
MultisigExternalSourceAccount()
} else {
null
}
}
override suspend fun getControllableExternalAccounts(accountIdsToQuery: Set<AccountIdKey>): List<ExternalControllableAccount> {
if (multisigChains.isEmpty()) return emptyList()
return multisigRepository.findMultisigAccounts(accountIdsToQuery)
.flatMap { discoveredMultisig ->
discoveredMultisig.allSignatories
.filter { it in accountIdsToQuery }
.mapNotNull { ourSignatory ->
MultisigExternalControllableAccount(
accountId = discoveredMultisig.accountId,
controllerAccountId = ourSignatory,
threshold = discoveredMultisig.threshold,
otherSignatories = discoveredMultisig.otherSignatories(ourSignatory),
addressScheme = discoveredMultisig.accountId.getAddressScheme() ?: return@mapNotNull null
)
}
}
}
private inner class MultisigExternalControllableAccount(
override val accountId: AccountIdKey,
override val controllerAccountId: AccountIdKey,
private val threshold: Int,
private val otherSignatories: List<AccountIdKey>,
private val addressScheme: AddressScheme
) : ExternalControllableAccount {
override fun isRepresentedBy(localAccount: MetaAccount): Boolean {
// Assuming accountId and controllerAccountId match, nothing else to check since both threshold and signers determine accountId
return localAccount is MultisigMetaAccount
}
override fun isAvailableOn(chain: Chain): Boolean {
return chain.id in multisigChainIds && chain.addressScheme == addressScheme
}
override suspend fun addControlledAccount(
controller: MetaAccount,
identity: Identity?,
position: Int,
missingAccountChain: Chain,
): AddAccountResult.AccountAdded {
val newId = addMultisig(controller, identity, position, missingAccountChain)
return AddAccountResult.AccountAdded(newId, LightMetaAccount.Type.MULTISIG)
}
override fun dispatchChangesOriginFilters(): Boolean {
return true
}
private suspend fun addMultisig(
controller: MetaAccount,
identity: Identity?,
position: Int,
chain: Chain
): Long {
return when (controller.type) {
LightMetaAccount.Type.SECRETS,
LightMetaAccount.Type.WATCH_ONLY -> addMultisigForComplexSigner(controller, identity, position, chain)
LightMetaAccount.Type.PARITY_SIGNER,
LightMetaAccount.Type.POLKADOT_VAULT -> addUniversalMultisig(controller, identity, position)
LightMetaAccount.Type.LEDGER_LEGACY,
LightMetaAccount.Type.LEDGER -> addSingleChainMultisig(controller, identity, position, chain)
LightMetaAccount.Type.PROXIED -> addSingleChainMultisig(controller, identity, position, chain)
LightMetaAccount.Type.MULTISIG -> addMultisigForComplexSigner(controller, identity, position, chain)
}
}
private suspend fun addMultisigForComplexSigner(
controller: MetaAccount,
identity: Identity?,
position: Int,
chain: Chain
): Long {
return if (controller.chainAccounts.isEmpty()) {
addUniversalMultisig(controller, identity, position)
} else {
addSingleChainMultisig(controller, identity, position, chain)
}
}
private suspend fun addSingleChainMultisig(
controller: MetaAccount,
identity: Identity?,
position: Int,
chain: Chain
): Long {
val metaAccount = createSingleChainMetaAccount(controller.id, identity, position)
return accountDao.insertMetaAndChainAccounts(metaAccount) { newId ->
listOf(createChainAccount(newId, chain))
}
}
private suspend fun addUniversalMultisig(
controller: MetaAccount,
identity: Identity?,
position: Int
): Long {
val metaAccount = createUniversalMetaAccount(controller.id, identity, position)
return accountDao.insertMetaAccount(metaAccount)
}
private fun createUniversalMetaAccount(
controllerMetaId: Long,
identity: Identity?,
position: Int
): MetaAccountLocal {
return MetaAccountLocal(
substratePublicKey = null,
substrateCryptoType = null,
substrateAccountId = substrateAccountId(),
ethereumPublicKey = null,
ethereumAddress = ethereumAddress(),
name = accountName(identity),
parentMetaId = controllerMetaId,
isSelected = false,
position = position,
type = MetaAccountLocal.Type.MULTISIG,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = typeExtras()
)
}
private fun createSingleChainMetaAccount(
controllerMetaId: Long,
identity: Identity?,
position: Int
): MetaAccountLocal {
return MetaAccountLocal(
substratePublicKey = null,
substrateCryptoType = null,
substrateAccountId = null,
ethereumPublicKey = null,
ethereumAddress = null,
name = accountName(identity),
parentMetaId = controllerMetaId,
isSelected = false,
position = position,
type = MetaAccountLocal.Type.MULTISIG,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = typeExtras()
)
}
private fun createChainAccount(
multisigId: Long,
chain: Chain
): ChainAccountLocal {
return ChainAccountLocal(
metaId = multisigId,
chainId = chain.id,
publicKey = null,
accountId = accountId.value,
cryptoType = null
)
}
private fun ethereumAddress(): ByteArray? {
return accountId.value.takeIf { addressScheme.isEvm() }
}
private fun substrateAccountId(): ByteArray? {
return accountId.value.takeIf { addressScheme.isSubstrate() }
}
private fun accountName(identity: Identity?): String {
if (identity != null) return identity.name
val addressFormat = AddressFormat.defaultForScheme(addressScheme)
return addressFormat.addressOf(accountId).value
}
private fun typeExtras(): String {
val extras = MultisigTypeExtras(
otherSignatories,
threshold,
signatoryAccountId = controllerAccountId
)
return gson.toJson(extras)
}
}
private class MultisigExternalSourceAccount : ExternalSourceCreatedAccount {
override fun canControl(candidate: ExternalControllableAccount): Boolean {
return true
}
}
}

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