mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 11:25:53 +00:00
feat: Bitcoin key derivation, secrets storage, and chain-family plumbing
Derives a BIP84 (native SegWit) keypair alongside the existing substrate/ ethereum/tron ones, stores it in MetaAccountSecrets/CloudBackup, persists bitcoinPublicKey/bitcoinAddress on meta_accounts (DB migration 74->75), and threads isBitcoinBased through Chain/ChainLocal/mappers/ChainExt/ChainRegistry the same way isTronBased was wired. Adds BitcoinAddressBackfillMigration, mirroring TronAddressBackfillMigration, so existing wallets get a Bitcoin address without re-import.
This commit is contained in:
+20
@@ -28,6 +28,9 @@ object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
||||
|
||||
val TronKeypair by schema(KeyPairSchema).optional()
|
||||
val TronDerivationPath by string().optional()
|
||||
|
||||
val BitcoinKeypair by schema(KeyPairSchema).optional()
|
||||
val BitcoinDerivationPath by string().optional()
|
||||
}
|
||||
|
||||
object ChainAccountSecrets : Schema<ChainAccountSecrets>() {
|
||||
@@ -47,6 +50,8 @@ fun MetaAccountSecrets(
|
||||
ethereumDerivationPath: String? = null,
|
||||
tronKeypair: Keypair? = null,
|
||||
tronDerivationPath: String? = null,
|
||||
bitcoinKeypair: Keypair? = null,
|
||||
bitcoinDerivationPath: String? = null,
|
||||
): EncodableStruct<MetaAccountSecrets> = MetaAccountSecrets { secrets ->
|
||||
secrets[Entropy] = entropy
|
||||
secrets[SubstrateSeed] = substrateSeed
|
||||
@@ -75,6 +80,15 @@ fun MetaAccountSecrets(
|
||||
}
|
||||
}
|
||||
secrets[TronDerivationPath] = tronDerivationPath
|
||||
|
||||
secrets[BitcoinKeypair] = bitcoinKeypair?.let {
|
||||
KeyPairSchema { keypair ->
|
||||
keypair[PublicKey] = it.publicKey
|
||||
keypair[PrivateKey] = it.privateKey
|
||||
keypair[Nonce] = null // bitcoin uses secp256k1 (like ethereum/tron), so nonce is always null
|
||||
}
|
||||
}
|
||||
secrets[BitcoinDerivationPath] = bitcoinDerivationPath
|
||||
}
|
||||
|
||||
fun ChainAccountSecrets(
|
||||
@@ -103,6 +117,9 @@ val EncodableStruct<MetaAccountSecrets>.ethereumDerivationPath
|
||||
val EncodableStruct<MetaAccountSecrets>.tronDerivationPath
|
||||
get() = get(MetaAccountSecrets.TronDerivationPath)
|
||||
|
||||
val EncodableStruct<MetaAccountSecrets>.bitcoinDerivationPath
|
||||
get() = get(MetaAccountSecrets.BitcoinDerivationPath)
|
||||
|
||||
val EncodableStruct<MetaAccountSecrets>.entropy
|
||||
get() = get(MetaAccountSecrets.Entropy)
|
||||
|
||||
@@ -118,6 +135,9 @@ val EncodableStruct<MetaAccountSecrets>.ethereumKeypair
|
||||
val EncodableStruct<MetaAccountSecrets>.tronKeypair
|
||||
get() = get(MetaAccountSecrets.TronKeypair)
|
||||
|
||||
val EncodableStruct<MetaAccountSecrets>.bitcoinKeypair
|
||||
get() = get(MetaAccountSecrets.BitcoinKeypair)
|
||||
|
||||
val EncodableStruct<ChainAccountSecrets>.derivationPath
|
||||
get() = get(ChainAccountSecrets.DerivationPath)
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ fun chainOf(
|
||||
isTestNet = false,
|
||||
isEthereumBased = false,
|
||||
isTronBased = false,
|
||||
isBitcoinBased = false,
|
||||
hasCrowdloans = false,
|
||||
additional = "",
|
||||
governance = "governance",
|
||||
|
||||
@@ -94,6 +94,7 @@ import io.novafoundation.nova.core_db.migrations.AddStakingTypeToTotalRewards_44
|
||||
import io.novafoundation.nova.core_db.migrations.AddSwapOption_48_49
|
||||
import io.novafoundation.nova.core_db.migrations.AddTransactionVersionToRuntime_50_51
|
||||
import io.novafoundation.nova.core_db.migrations.AddTransferApisTable_29_30
|
||||
import io.novafoundation.nova.core_db.migrations.AddBitcoinSupport_74_75
|
||||
import io.novafoundation.nova.core_db.migrations.AddTronSupport_73_74
|
||||
import io.novafoundation.nova.core_db.migrations.AddTypeExtrasToMetaAccount_68_69
|
||||
import io.novafoundation.nova.core_db.migrations.AddVersioningToGovernanceDapps_32_33
|
||||
@@ -167,7 +168,7 @@ import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal
|
||||
|
||||
@Database(
|
||||
version = 74,
|
||||
version = 75,
|
||||
entities = [
|
||||
AccountLocal::class,
|
||||
NodeLocal::class,
|
||||
@@ -273,6 +274,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
.addMigrations(AddTypeExtrasToMetaAccount_68_69, AddMultisigCalls_69_70, AddMultisigSupportFlag_70_71)
|
||||
.addMigrations(AddGifts_71_72, AddFieldsToContributions)
|
||||
.addMigrations(AddTronSupport_73_74)
|
||||
.addMigrations(AddBitcoinSupport_74_75)
|
||||
.build()
|
||||
}
|
||||
return instance!!
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.core_db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val AddBitcoinSupport_74_75 = object : Migration(74, 75) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE chains ADD COLUMN isBitcoinBased INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
db.execSQL("ALTER TABLE meta_accounts ADD COLUMN bitcoinPublicKey BLOB")
|
||||
db.execSQL("ALTER TABLE meta_accounts ADD COLUMN bitcoinAddress BLOB")
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ data class ChainLocal(
|
||||
val isEthereumBased: Boolean,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
val isTronBased: Boolean,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
val isBitcoinBased: Boolean,
|
||||
val isTestNet: Boolean,
|
||||
@ColumnInfo(defaultValue = "1")
|
||||
val hasSubstrateRuntime: Boolean,
|
||||
|
||||
+40
-1
@@ -39,6 +39,8 @@ class MetaAccountLocal(
|
||||
val typeExtras: SerializedJson?,
|
||||
val tronPublicKey: ByteArray? = null,
|
||||
val tronAddress: ByteArray? = null,
|
||||
val bitcoinPublicKey: ByteArray? = null,
|
||||
val bitcoinAddress: ByteArray? = null,
|
||||
) {
|
||||
|
||||
enum class Status {
|
||||
@@ -59,6 +61,9 @@ class MetaAccountLocal(
|
||||
const val TRON_PUBKEY = "tronPublicKey"
|
||||
const val TRON_ADDRESS = "tronAddress"
|
||||
|
||||
const val BITCOIN_PUBKEY = "bitcoinPublicKey"
|
||||
const val BITCOIN_ADDRESS = "bitcoinAddress"
|
||||
|
||||
const val NAME = "name"
|
||||
const val IS_SELECTED = "isSelected"
|
||||
const val POSITION = "position"
|
||||
@@ -90,7 +95,37 @@ class MetaAccountLocal(
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
typeExtras = typeExtras,
|
||||
tronPublicKey = tronPublicKey,
|
||||
tronAddress = tronAddress
|
||||
tronAddress = tronAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
).also {
|
||||
it.id = id
|
||||
}
|
||||
}
|
||||
|
||||
// We do not use copy as we need explicitly set id
|
||||
fun addBitcoinAccount(
|
||||
bitcoinPublicKey: ByteArray,
|
||||
bitcoinAddress: ByteArray,
|
||||
): MetaAccountLocal {
|
||||
return MetaAccountLocal(
|
||||
substratePublicKey = substratePublicKey,
|
||||
substrateCryptoType = substrateCryptoType,
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
ethereumAddress = ethereumAddress,
|
||||
name = name,
|
||||
parentMetaId = parentMetaId,
|
||||
isSelected = isSelected,
|
||||
position = position,
|
||||
type = type,
|
||||
status = status,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
typeExtras = typeExtras,
|
||||
tronPublicKey = tronPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
).also {
|
||||
it.id = id
|
||||
}
|
||||
@@ -109,6 +144,8 @@ class MetaAccountLocal(
|
||||
if (!ethereumAddress.contentEquals(other.ethereumAddress)) return false
|
||||
if (!tronPublicKey.contentEquals(other.tronPublicKey)) return false
|
||||
if (!tronAddress.contentEquals(other.tronAddress)) return false
|
||||
if (!bitcoinPublicKey.contentEquals(other.bitcoinPublicKey)) return false
|
||||
if (!bitcoinAddress.contentEquals(other.bitcoinAddress)) return false
|
||||
if (name != other.name) return false
|
||||
if (parentMetaId != other.parentMetaId) return false
|
||||
if (isSelected != other.isSelected) return false
|
||||
@@ -130,6 +167,8 @@ class MetaAccountLocal(
|
||||
result = 31 * result + (ethereumAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (tronPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (tronAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (bitcoinPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (bitcoinAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + (parentMetaId?.hashCode() ?: 0)
|
||||
result = 31 * result + isSelected.hashCode()
|
||||
|
||||
+8
@@ -52,6 +52,10 @@ interface LightMetaAccount {
|
||||
*/
|
||||
val tronAddress: ByteArray?
|
||||
val tronPublicKey: ByteArray?
|
||||
|
||||
/** Bitcoin account id (HASH160 of the compressed pubkey - see [io.novafoundation.nova.common.utils.hash160]). */
|
||||
val bitcoinAddress: ByteArray?
|
||||
val bitcoinPublicKey: ByteArray?
|
||||
val isSelected: Boolean
|
||||
val name: String
|
||||
val type: Type
|
||||
@@ -90,6 +94,8 @@ fun LightMetaAccount(
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
bitcoinAddress: ByteArray? = null,
|
||||
bitcoinPublicKey: ByteArray? = null,
|
||||
) = object : LightMetaAccount {
|
||||
override val id: Long = id
|
||||
override val globallyUniqueId: String = globallyUniqueId
|
||||
@@ -100,6 +106,8 @@ fun LightMetaAccount(
|
||||
override val ethereumPublicKey: ByteArray? = ethereumPublicKey
|
||||
override val tronAddress: ByteArray? = tronAddress
|
||||
override val tronPublicKey: ByteArray? = tronPublicKey
|
||||
override val bitcoinAddress: ByteArray? = bitcoinAddress
|
||||
override val bitcoinPublicKey: ByteArray? = bitcoinPublicKey
|
||||
override val isSelected: Boolean = isSelected
|
||||
override val name: String = name
|
||||
override val type: LightMetaAccount.Type = type
|
||||
|
||||
+22
-3
@@ -7,6 +7,8 @@ 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.bitcoinDerivationPath
|
||||
import io.novafoundation.nova.common.data.secrets.v2.bitcoinKeypair
|
||||
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
|
||||
@@ -90,6 +92,7 @@ class RealLocalAccountsCloudBackupFacade(
|
||||
substrate = baseSecrets.getSubstrateBackupSecrets(),
|
||||
ethereum = baseSecrets.getEthereumBackupSecrets(),
|
||||
chainAccounts = emptyList(),
|
||||
bitcoin = baseSecrets.getBitcoinBackupSecrets(),
|
||||
)
|
||||
|
||||
return CloudBackup(
|
||||
@@ -357,6 +360,7 @@ class RealLocalAccountsCloudBackupFacade(
|
||||
substrate = prepareSubstrateBackupSecrets(baseSecrets, joinedMetaAccountInfo),
|
||||
ethereum = baseSecrets.getEthereumBackupSecrets(),
|
||||
chainAccounts = chainAccountsFromChainSecrets + chainAccountFromAdditionalSecrets,
|
||||
bitcoin = baseSecrets.getBitcoinBackupSecrets(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -466,7 +470,9 @@ class RealLocalAccountsCloudBackupFacade(
|
||||
substrateKeyPair = substrate?.keypair?.toLocalKeyPair() ?: return null,
|
||||
substrateDerivationPath = substrate?.derivationPath,
|
||||
ethereumKeypair = ethereum?.keypair?.toLocalKeyPair(),
|
||||
ethereumDerivationPath = ethereum?.derivationPath
|
||||
ethereumDerivationPath = ethereum?.derivationPath,
|
||||
bitcoinKeypair = bitcoin?.keypair?.toLocalKeyPair(),
|
||||
bitcoinDerivationPath = bitcoin?.derivationPath
|
||||
)
|
||||
}
|
||||
|
||||
@@ -479,6 +485,15 @@ class RealLocalAccountsCloudBackupFacade(
|
||||
)
|
||||
}
|
||||
|
||||
private fun EncodableStruct<MetaAccountSecrets>?.getBitcoinBackupSecrets(): CloudBackup.WalletPrivateInfo.BitcoinSecrets? {
|
||||
if (this == null) return null
|
||||
|
||||
return CloudBackup.WalletPrivateInfo.BitcoinSecrets(
|
||||
keypair = bitcoinKeypair?.toBackupKeypairSecrets() ?: return null,
|
||||
derivationPath = bitcoinDerivationPath
|
||||
)
|
||||
}
|
||||
|
||||
private fun EncodableStruct<MetaAccountSecrets>?.getSubstrateBackupSecrets(): CloudBackup.WalletPrivateInfo.SubstrateSecrets? {
|
||||
if (this == null) return null
|
||||
|
||||
@@ -520,7 +535,9 @@ class RealLocalAccountsCloudBackupFacade(
|
||||
ethereumPublicKey = metaAccount.ethereumPublicKey,
|
||||
name = metaAccount.name,
|
||||
type = metaAccount.type.toBackupWalletType() ?: return null,
|
||||
chainAccounts = chainAccounts.mapToSet { chainAccount -> chainAccount.toBackupPublicChainAccount(chainsById) }
|
||||
chainAccounts = chainAccounts.mapToSet { chainAccount -> chainAccount.toBackupPublicChainAccount(chainsById) },
|
||||
bitcoinAddress = metaAccount.bitcoinAddress,
|
||||
bitcoinPublicKey = metaAccount.bitcoinPublicKey
|
||||
)
|
||||
}
|
||||
|
||||
@@ -542,7 +559,9 @@ class RealLocalAccountsCloudBackupFacade(
|
||||
isSelected = isSelected,
|
||||
position = accountPosition,
|
||||
status = MetaAccountLocal.Status.ACTIVE,
|
||||
typeExtras = null
|
||||
typeExtras = null,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey
|
||||
).also {
|
||||
if (localIdOverwrite != null) {
|
||||
it.id = localIdOverwrite
|
||||
|
||||
+14
@@ -65,6 +65,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
status = mapMetaAccountStateFromLocal(status),
|
||||
@@ -82,6 +84,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -101,6 +105,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -119,6 +125,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -138,6 +146,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -165,6 +175,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
status = mapMetaAccountStateFromLocal(status),
|
||||
@@ -183,6 +195,8 @@ class AccountMappers(
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
chainAccounts = chainAccounts,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
|
||||
+3
@@ -30,6 +30,7 @@ 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.BitcoinAddressBackfillMigration
|
||||
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
|
||||
@@ -64,10 +65,12 @@ class AccountDataSourceImpl(
|
||||
private val secretsMetaAccountLocalFactory: SecretsMetaAccountLocalFactory,
|
||||
secretStoreV1: SecretStoreV1,
|
||||
accountDataMigration: AccountDataMigration,
|
||||
private val bitcoinAddressBackfillMigration: BitcoinAddressBackfillMigration,
|
||||
) : AccountDataSource, SecretStoreV1 by secretStoreV1 {
|
||||
|
||||
init {
|
||||
migrateIfNeeded(accountDataMigration)
|
||||
async { bitcoinAddressBackfillMigration.migrate() }
|
||||
}
|
||||
|
||||
private fun migrateIfNeeded(migration: AccountDataMigration) = async {
|
||||
|
||||
+9
-1
@@ -2,6 +2,7 @@ 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.bitcoinPublicKeyToAccountId
|
||||
import io.novafoundation.nova.common.utils.substrateAccountId
|
||||
import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId
|
||||
import io.novafoundation.nova.core.model.CryptoType
|
||||
@@ -31,6 +32,7 @@ class RealSecretsMetaAccountLocalFactory : SecretsMetaAccountLocalFactory {
|
||||
val substratePublicKey = secrets[MetaAccountSecrets.SubstrateKeypair][KeyPairSchema.PublicKey]
|
||||
val ethereumPublicKey = secrets[MetaAccountSecrets.EthereumKeypair]?.get(KeyPairSchema.PublicKey)
|
||||
val tronPublicKey = secrets[MetaAccountSecrets.TronKeypair]?.get(KeyPairSchema.PublicKey)
|
||||
val bitcoinPublicKey = secrets[MetaAccountSecrets.BitcoinKeypair]?.get(KeyPairSchema.PublicKey)
|
||||
|
||||
return MetaAccountLocal(
|
||||
substratePublicKey = substratePublicKey,
|
||||
@@ -47,7 +49,13 @@ class RealSecretsMetaAccountLocalFactory : SecretsMetaAccountLocalFactory {
|
||||
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
|
||||
typeExtras = null,
|
||||
tronPublicKey = tronPublicKey,
|
||||
tronAddress = tronPublicKey?.tronPublicKeyToAccountId()
|
||||
tronAddress = tronPublicKey?.tronPublicKeyToAccountId(),
|
||||
bitcoinPublicKey = bitcoinPublicKey,
|
||||
// Bip32EcdsaKeypairFactory already yields a compressed (33-byte) public key (confirmed against
|
||||
// substrate-sdk-android's own source: ECDSAUtils.derivePublicKey -> compressedPublicKeyFromPrivate),
|
||||
// which is exactly the format both BIP143 (P2WPKH) and bitcoinPublicKeyToAccountId() require - no
|
||||
// extra compression/decompression step needed here, unlike Ethereum's uncompressed-key derivation.
|
||||
bitcoinAddress = bitcoinPublicKey?.bitcoinPublicKeyToAccountId()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
|
||||
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
|
||||
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
|
||||
import io.novafoundation.nova.common.data.secrets.v2.bitcoinDerivationPath
|
||||
import io.novafoundation.nova.common.data.secrets.v2.bitcoinKeypair
|
||||
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.mapKeypairStructToKeypair
|
||||
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.data.secrets.v2.tronDerivationPath
|
||||
import io.novafoundation.nova.common.data.secrets.v2.tronKeypair
|
||||
import io.novafoundation.nova.common.utils.bitcoinPublicKeyToAccountId
|
||||
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_impl.data.secrets.AccountSecretsFactory
|
||||
import io.novafoundation.nova.feature_account_impl.data.secrets.BITCOIN_DEFAULT_DERIVATION_PATH
|
||||
import io.novasama.substrate_sdk_android.encrypt.mnemonic.MnemonicCreator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val TAG = "BitcoinAddressBackfill"
|
||||
|
||||
/**
|
||||
* Idempotent, per-account backfill for accounts that don't yet have a Bitcoin keypair - mirrors
|
||||
* [TronAddressBackfillMigration]'s exact design (see that class for the full rationale): any account created
|
||||
* before Bitcoin support existed has no `MetaAccountSecrets.BitcoinKeypair`/`meta_accounts.bitcoinAddress` yet,
|
||||
* and this fills it in from the account's own mnemonic without requiring re-import.
|
||||
*
|
||||
* Deliberately has NO "have I already run once" flag, for the same reason as the Tron migration: a one-shot
|
||||
* flag that gets set even when backfill legitimately still didn't produce a key (e.g. a since-fixed bug kept
|
||||
* re-losing it) would permanently strand that account. This is cheap to call for an account that doesn't need
|
||||
* it, so it just runs unconditionally on every app start.
|
||||
*
|
||||
* Only touches accounts that are `Type.SECRETS` (mnemonic-derived) and still hold their `Entropy` in
|
||||
* [SecretStoreV2] - same restriction as the Tron migration, for the same reason (watch-only/Ledger/Json/
|
||||
* multisig/proxied accounts and raw-seed imports never had a Bitcoin-capable mnemonic to derive from).
|
||||
*/
|
||||
class BitcoinAddressBackfillMigration(
|
||||
private val secretStoreV2: SecretStoreV2,
|
||||
private val metaAccountDao: MetaAccountDao,
|
||||
private val accountSecretsFactory: AccountSecretsFactory,
|
||||
) {
|
||||
|
||||
suspend fun migrate() = withContext(Dispatchers.Default) {
|
||||
val secretsAccounts = metaAccountDao.getMetaAccounts().filter { it.type == MetaAccountLocal.Type.SECRETS }
|
||||
Log.d(TAG, "migrate() starting - ${secretsAccounts.size} SECRETS-type account(s): ${secretsAccounts.map { it.id }}")
|
||||
|
||||
secretsAccounts.forEach { account ->
|
||||
try {
|
||||
backfillIfNeeded(account)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "backfill failed for metaId=${account.id}, continuing with remaining accounts", e)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "migrate() done")
|
||||
}
|
||||
|
||||
private suspend fun backfillIfNeeded(account: MetaAccountLocal) {
|
||||
val secrets = secretStoreV2.getMetaAccountSecrets(account.id)
|
||||
if (secrets == null) {
|
||||
Log.d(TAG, "metaId=${account.id}: no stored secrets at all (watch-only/Ledger/etc) - skipping")
|
||||
return
|
||||
}
|
||||
val entropy = secrets.entropy
|
||||
if (entropy == null) {
|
||||
Log.d(TAG, "metaId=${account.id}: no entropy (raw-seed import, not a mnemonic) - skipping")
|
||||
return
|
||||
}
|
||||
if (secrets.bitcoinKeypair != null) {
|
||||
Log.d(TAG, "metaId=${account.id}: already has a BitcoinKeypair - skipping")
|
||||
return
|
||||
}
|
||||
val substrateCryptoType = account.substrateCryptoType
|
||||
if (substrateCryptoType == null) {
|
||||
Log.d(TAG, "metaId=${account.id}: substrateCryptoType is null - skipping")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "metaId=${account.id}: deriving Bitcoin keypair")
|
||||
|
||||
val mnemonic = MnemonicCreator.fromEntropy(entropy).words
|
||||
|
||||
val bitcoinChainSecrets = accountSecretsFactory.chainAccountSecrets(
|
||||
derivationPath = BITCOIN_DEFAULT_DERIVATION_PATH,
|
||||
accountSource = AccountSecretsFactory.AccountSource.Mnemonic(substrateCryptoType, mnemonic),
|
||||
isEthereum = true
|
||||
).secrets
|
||||
|
||||
val bitcoinKeypair = mapKeypairStructToKeypair(bitcoinChainSecrets[ChainAccountSecrets.Keypair])
|
||||
|
||||
val updatedSecrets = MetaAccountSecrets(
|
||||
substrateKeyPair = mapKeypairStructToKeypair(secrets.substrateKeypair),
|
||||
entropy = secrets.entropy,
|
||||
substrateSeed = secrets.seed,
|
||||
substrateDerivationPath = secrets.substrateDerivationPath,
|
||||
ethereumKeypair = secrets.ethereumKeypair?.let(::mapKeypairStructToKeypair),
|
||||
ethereumDerivationPath = secrets.ethereumDerivationPath,
|
||||
tronKeypair = secrets.tronKeypair?.let(::mapKeypairStructToKeypair),
|
||||
tronDerivationPath = secrets.tronDerivationPath,
|
||||
bitcoinKeypair = bitcoinKeypair,
|
||||
bitcoinDerivationPath = BITCOIN_DEFAULT_DERIVATION_PATH
|
||||
)
|
||||
|
||||
secretStoreV2.putMetaAccountSecrets(account.id, updatedSecrets)
|
||||
|
||||
val bitcoinAccountId = bitcoinKeypair.publicKey.bitcoinPublicKeyToAccountId()
|
||||
metaAccountDao.updateMetaAccount(account.id) { it.addBitcoinAccount(bitcoinKeypair.publicKey, bitcoinAccountId) }
|
||||
|
||||
Log.d(TAG, "metaId=${account.id}: backfilled successfully, bitcoinAddress set (${bitcoinAccountId.size} bytes)")
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -34,6 +34,14 @@ import kotlinx.coroutines.withContext
|
||||
*/
|
||||
const val TRON_DEFAULT_DERIVATION_PATH = "//44//195//0/0/0"
|
||||
|
||||
/**
|
||||
* BIP84 purpose (native SegWit), SLIP-44 coin type 0 (Bitcoin). Deliberately `//84//...`, not `//44//...` -
|
||||
* this app only supports native SegWit (bech32 `bc1q...`) addresses, not legacy/P2SH-SegWit, so the derivation
|
||||
* path signals that choice the same way real Bitcoin wallets do. Not user-configurable yet - always derived at
|
||||
* this fixed path (single address, no HD address-index rotation).
|
||||
*/
|
||||
const val BITCOIN_DEFAULT_DERIVATION_PATH = "//84//0//0/0/0"
|
||||
|
||||
class AccountSecretsFactory(
|
||||
private val JsonDecoder: JsonDecoder
|
||||
) {
|
||||
@@ -131,6 +139,7 @@ class AccountSecretsFactory(
|
||||
ethereumDerivationPath: String?,
|
||||
accountSource: AccountSource,
|
||||
tronDerivationPath: String? = TRON_DEFAULT_DERIVATION_PATH,
|
||||
bitcoinDerivationPath: String? = BITCOIN_DEFAULT_DERIVATION_PATH,
|
||||
): Result<MetaAccountSecrets> = withContext(Dispatchers.Default) {
|
||||
val (substrateSecrets, substrateCryptoType) = chainAccountSecrets(
|
||||
derivationPath = substrateDerivationPath,
|
||||
@@ -157,6 +166,16 @@ class AccountSecretsFactory(
|
||||
Bip32EcdsaKeypairFactory.generate(seed = seed, junctions = decodedTronDerivationPath?.junctions.orEmpty())
|
||||
}
|
||||
|
||||
// Bitcoin (native SegWit) also reuses the exact same secp256k1/BIP32 keypair generation as Ethereum/Tron -
|
||||
// only the SLIP-44 coin type (0) and BIP84 purpose differ. See BITCOIN_DEFAULT_DERIVATION_PATH's doc.
|
||||
val bitcoinKeypair = accountSource.castOrNull<AccountSource.Mnemonic>()?.let {
|
||||
val decodedBitcoinDerivationPath = decodeDerivationPath(bitcoinDerivationPath, ethereum = true)
|
||||
|
||||
val seed = deriveSeed(it.mnemonic, password = decodedBitcoinDerivationPath?.password, ethereum = true).seed
|
||||
|
||||
Bip32EcdsaKeypairFactory.generate(seed = seed, junctions = decodedBitcoinDerivationPath?.junctions.orEmpty())
|
||||
}
|
||||
|
||||
val secrets = MetaAccountSecrets(
|
||||
entropy = substrateSecrets[ChainAccountSecrets.Entropy],
|
||||
substrateSeed = substrateSecrets[ChainAccountSecrets.Seed],
|
||||
@@ -165,7 +184,9 @@ class AccountSecretsFactory(
|
||||
ethereumKeypair = ethereumKeypair,
|
||||
ethereumDerivationPath = ethereumDerivationPath,
|
||||
tronKeypair = tronKeypair,
|
||||
tronDerivationPath = tronDerivationPath
|
||||
tronDerivationPath = tronDerivationPath,
|
||||
bitcoinKeypair = bitcoinKeypair,
|
||||
bitcoinDerivationPath = bitcoinDerivationPath,
|
||||
)
|
||||
|
||||
Result(secrets = secrets, cryptoType = substrateCryptoType)
|
||||
|
||||
+14
-1
@@ -104,6 +104,7 @@ import io.novafoundation.nova.feature_account_impl.data.repository.datasource.Ac
|
||||
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.RealSecretsMetaAccountLocalFactory
|
||||
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.SecretsMetaAccountLocalFactory
|
||||
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.AccountDataMigration
|
||||
import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.BitcoinAddressBackfillMigration
|
||||
import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory
|
||||
import io.novafoundation.nova.feature_account_impl.data.signer.signingContext.SigningContextFactory
|
||||
import io.novafoundation.nova.feature_account_impl.di.AccountFeatureModule.BindsModule
|
||||
@@ -406,6 +407,7 @@ class AccountFeatureModule {
|
||||
secretsMetaAccountLocalFactory: SecretsMetaAccountLocalFactory,
|
||||
secretStoreV2: SecretStoreV2,
|
||||
accountMappers: AccountMappers,
|
||||
bitcoinAddressBackfillMigration: BitcoinAddressBackfillMigration,
|
||||
): AccountDataSource {
|
||||
return AccountDataSourceImpl(
|
||||
preferences,
|
||||
@@ -416,10 +418,21 @@ class AccountFeatureModule {
|
||||
secretStoreV2,
|
||||
secretsMetaAccountLocalFactory,
|
||||
secretStoreV1,
|
||||
accountDataMigration
|
||||
accountDataMigration,
|
||||
bitcoinAddressBackfillMigration
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideBitcoinAddressBackfillMigration(
|
||||
secretStoreV2: SecretStoreV2,
|
||||
metaAccountDao: MetaAccountDao,
|
||||
accountSecretsFactory: AccountSecretsFactory,
|
||||
): BitcoinAddressBackfillMigration {
|
||||
return BitcoinAddressBackfillMigration(secretStoreV2, metaAccountDao, accountSecretsFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideNodeHostValidator() = NodeHostValidator()
|
||||
|
||||
|
||||
+5
@@ -24,6 +24,8 @@ open class DefaultMetaAccount(
|
||||
override val parentMetaId: Long?,
|
||||
override val tronAddress: ByteArray? = null,
|
||||
override val tronPublicKey: ByteArray? = null,
|
||||
override val bitcoinAddress: ByteArray? = null,
|
||||
override val bitcoinPublicKey: ByteArray? = null,
|
||||
) : MetaAccount {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
@@ -34,6 +36,7 @@ open class DefaultMetaAccount(
|
||||
return when {
|
||||
hasChainAccountIn(chain.id) -> true
|
||||
chain.isTronBased -> tronAddress != null
|
||||
chain.isBitcoinBased -> bitcoinAddress != null
|
||||
chain.isEthereumBased -> ethereumAddress != null
|
||||
else -> substrateAccountId != null
|
||||
}
|
||||
@@ -43,6 +46,7 @@ open class DefaultMetaAccount(
|
||||
return when {
|
||||
hasChainAccountIn(chain.id) -> chainAccounts.getValue(chain.id).accountId
|
||||
chain.isTronBased -> tronAddress
|
||||
chain.isBitcoinBased -> bitcoinAddress
|
||||
chain.isEthereumBased -> ethereumAddress
|
||||
else -> substrateAccountId
|
||||
}
|
||||
@@ -52,6 +56,7 @@ open class DefaultMetaAccount(
|
||||
return when {
|
||||
hasChainAccountIn(chain.id) -> chainAccounts.getValue(chain.id).publicKey
|
||||
chain.isTronBased -> tronPublicKey
|
||||
chain.isBitcoinBased -> bitcoinPublicKey
|
||||
chain.isEthereumBased -> ethereumPublicKey
|
||||
else -> substratePublicKey
|
||||
}
|
||||
|
||||
+5
-1
@@ -24,6 +24,8 @@ class GenericLedgerMetaAccount(
|
||||
private val supportedGenericLedgerChains: Set<ChainId>,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
bitcoinAddress: ByteArray? = null,
|
||||
bitcoinPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -39,7 +41,9 @@ class GenericLedgerMetaAccount(
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey
|
||||
) {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
|
||||
+5
-1
@@ -24,6 +24,8 @@ class LegacyLedgerMetaAccount(
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
bitcoinAddress: ByteArray? = null,
|
||||
bitcoinPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -39,7 +41,9 @@ class LegacyLedgerMetaAccount(
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey
|
||||
) {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
|
||||
+5
-1
@@ -30,6 +30,8 @@ class RealMultisigMetaAccount(
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
bitcoinAddress: ByteArray? = null,
|
||||
bitcoinPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -45,7 +47,9 @@ class RealMultisigMetaAccount(
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey
|
||||
),
|
||||
MultisigMetaAccount {
|
||||
|
||||
|
||||
+1
-1
@@ -41,6 +41,6 @@ class PolkadotVaultMetaAccount(
|
||||
) {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
return !chain.isEthereumBased && !chain.isTronBased
|
||||
return !chain.isEthereumBased && !chain.isTronBased && !chain.isBitcoinBased
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -24,6 +24,8 @@ internal class RealProxiedMetaAccount(
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
bitcoinAddress: ByteArray? = null,
|
||||
bitcoinPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -39,7 +41,9 @@ internal class RealProxiedMetaAccount(
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey
|
||||
),
|
||||
ProxiedMetaAccount {
|
||||
|
||||
|
||||
+5
-1
@@ -25,6 +25,8 @@ class RealSecretsMetaAccount(
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
bitcoinAddress: ByteArray? = null,
|
||||
bitcoinPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -40,7 +42,9 @@ class RealSecretsMetaAccount(
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
tronPublicKey = tronPublicKey,
|
||||
bitcoinAddress = bitcoinAddress,
|
||||
bitcoinPublicKey = bitcoinPublicKey
|
||||
),
|
||||
SecretsMetaAccount {
|
||||
|
||||
|
||||
+19
-3
@@ -23,7 +23,11 @@ data class CloudBackup(
|
||||
val ethereumPublicKey: ByteArray?,
|
||||
val name: String,
|
||||
val type: Type,
|
||||
val chainAccounts: Set<ChainAccountInfo>
|
||||
val chainAccounts: Set<ChainAccountInfo>,
|
||||
// Nullable and defaulted so that Gson deserializing a backup written before Bitcoin support existed -
|
||||
// which has no such field in its JSON at all - lands on null here rather than failing.
|
||||
val bitcoinAddress: ByteArray? = null,
|
||||
val bitcoinPublicKey: ByteArray? = null,
|
||||
) : Identifiable {
|
||||
|
||||
override val identifier: String = walletId
|
||||
@@ -72,7 +76,9 @@ data class CloudBackup(
|
||||
ethereumPublicKey.contentEquals(other.ethereumPublicKey) &&
|
||||
name == other.name &&
|
||||
type == other.type &&
|
||||
chainAccounts == other.chainAccounts
|
||||
chainAccounts == other.chainAccounts &&
|
||||
bitcoinAddress.contentEquals(other.bitcoinAddress) &&
|
||||
bitcoinPublicKey.contentEquals(other.bitcoinPublicKey)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -85,6 +91,8 @@ data class CloudBackup(
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + type.hashCode()
|
||||
result = 31 * result + chainAccounts.hashCode()
|
||||
result = 31 * result + (bitcoinAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (bitcoinPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + identifier.hashCode()
|
||||
return result
|
||||
}
|
||||
@@ -100,6 +108,7 @@ data class CloudBackup(
|
||||
val substrate: SubstrateSecrets?,
|
||||
val ethereum: EthereumSecrets?,
|
||||
val chainAccounts: List<ChainAccountSecrets>,
|
||||
val bitcoin: BitcoinSecrets? = null,
|
||||
) : Identifiable {
|
||||
|
||||
override val identifier: String = walletId
|
||||
@@ -118,6 +127,7 @@ data class CloudBackup(
|
||||
if (substrate != other.substrate) return false
|
||||
if (ethereum != other.ethereum) return false
|
||||
if (chainAccounts != other.chainAccounts) return false
|
||||
if (bitcoin != other.bitcoin) return false
|
||||
return identifier == other.identifier
|
||||
}
|
||||
|
||||
@@ -127,6 +137,7 @@ data class CloudBackup(
|
||||
result = 31 * result + (substrate?.hashCode() ?: 0)
|
||||
result = 31 * result + (ethereum?.hashCode() ?: 0)
|
||||
result = 31 * result + chainAccounts.hashCode()
|
||||
result = 31 * result + (bitcoin?.hashCode() ?: 0)
|
||||
result = 31 * result + identifier.hashCode()
|
||||
return result
|
||||
}
|
||||
@@ -201,6 +212,11 @@ data class CloudBackup(
|
||||
val derivationPath: String?,
|
||||
)
|
||||
|
||||
data class BitcoinSecrets(
|
||||
val keypair: KeyPairSecrets,
|
||||
val derivationPath: String?,
|
||||
)
|
||||
|
||||
data class KeyPairSecrets(
|
||||
val publicKey: ByteArray,
|
||||
val privateKey: ByteArray,
|
||||
@@ -234,5 +250,5 @@ data class CloudBackup(
|
||||
}
|
||||
|
||||
fun CloudBackup.WalletPrivateInfo.isCompletelyEmpty(): Boolean {
|
||||
return entropy == null && substrate == null && ethereum == null && chainAccounts.isEmpty()
|
||||
return entropy == null && substrate == null && ethereum == null && bitcoin == null && chainAccounts.isEmpty()
|
||||
}
|
||||
|
||||
+1
@@ -125,6 +125,7 @@ class CustomChainFactory(
|
||||
types = prefilledChain?.types,
|
||||
isEthereumBased = isEthereumBased,
|
||||
isTronBased = false,
|
||||
isBitcoinBased = false,
|
||||
isTestNet = prefilledChain?.isTestNet.orFalse(),
|
||||
source = Chain.Source.CUSTOM,
|
||||
hasSubstrateRuntime = hasSubstrateRuntime,
|
||||
|
||||
@@ -272,6 +272,7 @@ fun Chain.requireGenesisHash() = requireNotNull(genesisHash)
|
||||
fun Chain.addressOf(accountId: ByteArray): String {
|
||||
return when {
|
||||
isTronBased -> accountId.toTronAddress()
|
||||
isBitcoinBased -> accountId.toBitcoinAddress()
|
||||
isEthereumBased -> accountId.toEthereumAddress()
|
||||
else -> accountId.toAddress(addressPrefix.toShort())
|
||||
}
|
||||
@@ -282,7 +283,7 @@ fun Chain.addressOf(accountId: AccountIdKey): String {
|
||||
}
|
||||
|
||||
fun Chain.legacyAddressOfOrNull(accountId: ByteArray): String? {
|
||||
return if (isEthereumBased || isTronBased) {
|
||||
return if (isEthereumBased || isTronBased || isBitcoinBased) {
|
||||
null
|
||||
} else {
|
||||
legacyAddressPrefix?.let { accountId.toAddress(it.toShort()) }
|
||||
@@ -296,6 +297,7 @@ fun ByteArray.toEthereumAddress(): String {
|
||||
fun Chain.accountIdOf(address: String): ByteArray {
|
||||
return when {
|
||||
isTronBased -> address.tronAddressToAccountId()
|
||||
isBitcoinBased -> address.bitcoinAddressToAccountId()
|
||||
isEthereumBased -> address.asEthereumAddress().toAccountId().value
|
||||
else -> address.toAccountId()
|
||||
}
|
||||
@@ -329,6 +331,7 @@ fun Chain.accountIdOrNull(address: String): ByteArray? {
|
||||
|
||||
fun Chain.emptyAccountId() = when {
|
||||
isTronBased -> emptyTronAccountId()
|
||||
isBitcoinBased -> emptyBitcoinAccountId()
|
||||
isEthereumBased -> emptyEthereumAccountId()
|
||||
else -> emptySubstrateAccountId()
|
||||
}
|
||||
@@ -342,6 +345,7 @@ fun Chain.accountIdOrDefault(maybeAddress: String): ByteArray {
|
||||
fun Chain.accountIdOf(publicKey: ByteArray): ByteArray {
|
||||
return when {
|
||||
isTronBased -> publicKey.tronPublicKeyToAccountId()
|
||||
isBitcoinBased -> publicKey.bitcoinPublicKeyToAccountId()
|
||||
isEthereumBased -> publicKey.asEthereumPublicKey().toAccountId().value
|
||||
else -> publicKey.substrateAccountId()
|
||||
}
|
||||
@@ -361,13 +365,15 @@ fun Chain.multiAddressOf(accountId: ByteArray): MultiAddress {
|
||||
|
||||
fun Chain.isValidAddress(address: String): Boolean {
|
||||
return runCatching {
|
||||
if (isEthereumBased) {
|
||||
address.asEthereumAddress().isValid()
|
||||
} else {
|
||||
address.toAccountId() // verify supplied address can be converted to account id
|
||||
when {
|
||||
isBitcoinBased -> address.isValidBitcoinAddress()
|
||||
isEthereumBased -> address.asEthereumAddress().isValid()
|
||||
else -> {
|
||||
address.toAccountId() // verify supplied address can be converted to account id
|
||||
|
||||
addressPrefix.toShort() == address.addressPrefix() ||
|
||||
legacyAddressPrefix?.toShort() == address.addressPrefix()
|
||||
addressPrefix.toShort() == address.addressPrefix() ||
|
||||
legacyAddressPrefix?.toShort() == address.addressPrefix()
|
||||
}
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
@@ -221,11 +221,11 @@ class ChainRegistry(
|
||||
}
|
||||
|
||||
private suspend fun registerConnection(chain: Chain): ChainConnection? {
|
||||
// Tron nodes are plain REST APIs (TronGrid), not WSS JSON-RPC endpoints - ChainConnection's
|
||||
// SocketService can only speak the latter, so attempting to set one up here would hang
|
||||
// indefinitely instead of failing fast. Tron balance/transfer operations already go through
|
||||
// their own dedicated TronGridApi client, independent of this connection pool.
|
||||
if (chain.isTronBased) return null
|
||||
// Tron/Bitcoin nodes are plain REST APIs (TronGrid / mempool.space), not WSS JSON-RPC endpoints -
|
||||
// ChainConnection's SocketService can only speak the latter, so attempting to set one up here would
|
||||
// hang indefinitely instead of failing fast. Balance/transfer operations for both go through their
|
||||
// own dedicated REST API clients, independent of this connection pool.
|
||||
if (chain.isTronBased || chain.isBitcoinBased) return null
|
||||
|
||||
val connection = connectionPool.setupConnection(chain)
|
||||
|
||||
|
||||
+1
@@ -128,6 +128,7 @@ fun mapChainToLocal(chain: Chain, gson: Gson): ChainLocal {
|
||||
legacyPrefix = chain.legacyAddressPrefix,
|
||||
isEthereumBased = chain.isEthereumBased,
|
||||
isTronBased = chain.isTronBased,
|
||||
isBitcoinBased = chain.isBitcoinBased,
|
||||
isTestNet = chain.isTestNet,
|
||||
hasSubstrateRuntime = chain.hasSubstrateRuntime,
|
||||
pushSupport = chain.pushSupport,
|
||||
|
||||
+1
@@ -276,6 +276,7 @@ fun mapChainLocalToChain(
|
||||
legacyAddressPrefix = legacyPrefix,
|
||||
isEthereumBased = isEthereumBased,
|
||||
isTronBased = isTronBased,
|
||||
isBitcoinBased = isBitcoinBased,
|
||||
isTestNet = isTestNet,
|
||||
hasCrowdloans = hasCrowdloans,
|
||||
pushSupport = pushSupport,
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemot
|
||||
|
||||
private const val ETHEREUM_OPTION = "ethereumBased"
|
||||
private const val TRON_OPTION = "tronBased"
|
||||
private const val BITCOIN_OPTION = "bitcoinBased"
|
||||
private const val CROWDLOAN_OPTION = "crowdloans"
|
||||
private const val TESTNET_OPTION = "testnet"
|
||||
private const val PROXY_OPTION = "proxy"
|
||||
@@ -92,6 +93,7 @@ fun mapRemoteChainToLocal(
|
||||
legacyPrefix = legacyAddressPrefix,
|
||||
isEthereumBased = ETHEREUM_OPTION in optionsOrEmpty,
|
||||
isTronBased = TRON_OPTION in optionsOrEmpty,
|
||||
isBitcoinBased = BITCOIN_OPTION in optionsOrEmpty,
|
||||
isTestNet = TESTNET_OPTION in optionsOrEmpty,
|
||||
hasCrowdloans = CROWDLOAN_OPTION in optionsOrEmpty,
|
||||
supportProxy = PROXY_OPTION in optionsOrEmpty,
|
||||
|
||||
@@ -33,6 +33,7 @@ data class Chain(
|
||||
val types: Types?,
|
||||
val isEthereumBased: Boolean,
|
||||
val isTronBased: Boolean,
|
||||
val isBitcoinBased: Boolean,
|
||||
val isTestNet: Boolean,
|
||||
val source: Source,
|
||||
val hasSubstrateRuntime: Boolean,
|
||||
|
||||
Reference in New Issue
Block a user