mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
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:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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 />
|
||||
+103
@@ -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)
|
||||
}
|
||||
}
|
||||
+54
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
}
|
||||
}
|
||||
+621
@@ -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
|
||||
}
|
||||
}
|
||||
+205
@@ -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
|
||||
}
|
||||
}
|
||||
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
+164
@@ -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()
|
||||
}
|
||||
}
|
||||
+436
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+45
@@ -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
|
||||
}
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -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()
|
||||
}
|
||||
}
|
||||
+77
@@ -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)
|
||||
}
|
||||
}
|
||||
+35
@@ -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())
|
||||
}
|
||||
}
|
||||
+64
@@ -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)
|
||||
}
|
||||
}
|
||||
+79
@@ -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()
|
||||
}
|
||||
}
|
||||
+7
@@ -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
|
||||
+60
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -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
|
||||
+122
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+207
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+168
@@ -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)
|
||||
}
|
||||
+30
@@ -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>>
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
@@ -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 }
|
||||
}
|
||||
}
|
||||
+25
@@ -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>
|
||||
}
|
||||
+36
@@ -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()
|
||||
}
|
||||
+40
@@ -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()
|
||||
}
|
||||
+23
@@ -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
|
||||
)
|
||||
}
|
||||
+11
@@ -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)
|
||||
}
|
||||
+32
@@ -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
|
||||
)
|
||||
+34
@@ -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"])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+11
@@ -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?
|
||||
)
|
||||
+48
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
+46
@@ -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
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_account_impl.data.network.blockchain
|
||||
|
||||
interface AccountSubstrateSource {
|
||||
|
||||
/**
|
||||
* @throws NovaException
|
||||
*/
|
||||
suspend fun getNodeNetworkType(nodeHost: String): String
|
||||
}
|
||||
+18
@@ -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())
|
||||
}
|
||||
}
|
||||
+78
@@ -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()
|
||||
}
|
||||
+73
@@ -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)
|
||||
}
|
||||
}
|
||||
+13
@@ -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>
|
||||
}
|
||||
+30
@@ -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()
|
||||
}
|
||||
+16
@@ -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,
|
||||
)
|
||||
+44
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -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
|
||||
)
|
||||
}
|
||||
+12
@@ -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,
|
||||
)
|
||||
+396
@@ -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)
|
||||
}
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
}
|
||||
+179
@@ -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
|
||||
}
|
||||
}
|
||||
+29
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+21
@@ -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
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
}
|
||||
+70
@@ -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)
|
||||
}
|
||||
}
|
||||
+89
@@ -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)
|
||||
}
|
||||
}
|
||||
+60
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -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()
|
||||
}
|
||||
+55
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+99
@@ -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)
|
||||
}
|
||||
}
|
||||
+39
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+35
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
}
|
||||
+74
@@ -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)
|
||||
}
|
||||
}
|
||||
+123
@@ -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))
|
||||
}
|
||||
+328
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+93
@@ -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)
|
||||
}
|
||||
}
|
||||
+12
@@ -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>
|
||||
)
|
||||
+11
@@ -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>
|
||||
)
|
||||
+168
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -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
|
||||
)
|
||||
}
|
||||
+99
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
}
|
||||
+203
@@ -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)
|
||||
}
|
||||
}
|
||||
+9
@@ -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)
|
||||
}
|
||||
+137
@@ -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)
|
||||
}
|
||||
}
|
||||
+31
@@ -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)
|
||||
}
|
||||
+20
@@ -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()
|
||||
}
|
||||
+23
@@ -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)
|
||||
}
|
||||
+19
@@ -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
|
||||
+244
@@ -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
|
||||
}
|
||||
}
|
||||
+95
@@ -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)
|
||||
}
|
||||
}
|
||||
+14
@@ -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) }
|
||||
}
|
||||
}
|
||||
+7
@@ -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
|
||||
}
|
||||
+10
@@ -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
|
||||
}
|
||||
}
|
||||
+20
@@ -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
|
||||
}
|
||||
}
|
||||
+189
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -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))
|
||||
}
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
+17
@@ -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)
|
||||
}
|
||||
}
|
||||
+50
@@ -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()
|
||||
}
|
||||
}
|
||||
+45
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -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)
|
||||
}
|
||||
+264
@@ -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
Reference in New Issue
Block a user