Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parity signer/ethereum poc #481

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ buildscript {

web3jVersion = '4.8.0'

fearlessLibVersion = '1.6.0'
fearlessLibVersion = '1.6.2'

gifVersion = '1.2.19'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.novafoundation.nova.common.address.format

interface AddressFormat {

@JvmInline
value class PublicKey(val value: ByteArray)

@JvmInline
value class AccountId(val value: ByteArray)

@JvmInline
value class Address(val value: String)

fun addressOf(accountId: AccountId): Address

fun accountIdOf(address: Address): AccountId

fun accountIdOf(publicKey: PublicKey): AccountId

fun isValidAddress(address: Address): Boolean
}

fun ByteArray.asPublicKey() = AddressFormat.PublicKey(this)
fun ByteArray.asAccountId() = AddressFormat.AccountId(this)
fun String.asAddress() = AddressFormat.Address(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.novafoundation.nova.common.address.format

import jp.co.soramitsu.fearless_utils.extensions.asEthereumAccountId
import jp.co.soramitsu.fearless_utils.extensions.asEthereumAddress
import jp.co.soramitsu.fearless_utils.extensions.asEthereumPublicKey
import jp.co.soramitsu.fearless_utils.extensions.isValid
import jp.co.soramitsu.fearless_utils.extensions.toAccountId
import jp.co.soramitsu.fearless_utils.extensions.toAddress

class EthereumAddressFormat : AddressFormat {

override fun addressOf(accountId: AddressFormat.AccountId): AddressFormat.Address {
return accountId.value.asEthereumAccountId()
.toAddress().value.asAddress()
}

override fun accountIdOf(address: AddressFormat.Address): AddressFormat.AccountId {
return address.value.asEthereumAddress()
.toAccountId().value.asAccountId()
}

override fun accountIdOf(publicKey: AddressFormat.PublicKey): AddressFormat.AccountId {
return publicKey.value.asEthereumPublicKey()
.toAccountId().value.asAccountId()
}

override fun isValidAddress(address: AddressFormat.Address): Boolean {
return address.value.asEthereumAddress().isValid()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.novafoundation.nova.common.address.format

import io.novafoundation.nova.common.utils.DEFAULT_PREFIX
import io.novafoundation.nova.common.utils.isValidSS58Address
import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder
import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.addressPrefix
import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.publicKeyToSubstrateAccountId
import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId
import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAddress

class SpecificSubstrateAddressFormat(private val ss58Prefix: Short) : AddressFormat {

override fun addressOf(accountId: AddressFormat.AccountId): AddressFormat.Address {
return accountId.value.toAddress(ss58Prefix).asAddress()
}

override fun accountIdOf(address: AddressFormat.Address): AddressFormat.AccountId {
return address.value.toAccountId().asAccountId()
}

override fun accountIdOf(publicKey: AddressFormat.PublicKey): AddressFormat.AccountId {
return publicKey.value.publicKeyToSubstrateAccountId().asAccountId()
}

override fun isValidAddress(address: AddressFormat.Address): Boolean {
return runCatching {
address.value.isValidSS58Address()
&& address.value.addressPrefix() == ss58Prefix
}.getOrDefault(false)
}
}

class AnySubstrateChainAddressFormat : AddressFormat {

override fun addressOf(accountId: AddressFormat.AccountId): AddressFormat.Address {
return accountId.value.toAddress(SS58Encoder.DEFAULT_PREFIX).asAddress()
}

override fun accountIdOf(address: AddressFormat.Address): AddressFormat.AccountId {
return address.value.toAccountId().asAccountId()
}

override fun accountIdOf(publicKey: AddressFormat.PublicKey): AddressFormat.AccountId {
return publicKey.value.publicKeyToSubstrateAccountId().asAccountId()
}

override fun isValidAddress(address: AddressFormat.Address): Boolean {
return address.value.isValidSS58Address()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import io.novafoundation.nova.common.data.network.runtime.binding.bindNullableNu
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberConstant
import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible
import io.novafoundation.nova.core.model.Node
import jp.co.soramitsu.fearless_utils.encrypt.SignatureVerifier
import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper
import jp.co.soramitsu.fearless_utils.encrypt.Signer
import jp.co.soramitsu.fearless_utils.encrypt.junction.BIP32JunctionDecoder
import jp.co.soramitsu.fearless_utils.encrypt.mnemonic.Mnemonic
import jp.co.soramitsu.fearless_utils.encrypt.seed.SeedFactory
import jp.co.soramitsu.fearless_utils.extensions.asEthereumAddress
import jp.co.soramitsu.fearless_utils.extensions.asEthereumPublicKey
import jp.co.soramitsu.fearless_utils.extensions.fromHex
import jp.co.soramitsu.fearless_utils.extensions.toAccountId
import jp.co.soramitsu.fearless_utils.extensions.toHexString
import jp.co.soramitsu.fearless_utils.hash.Hasher.blake2b256
import jp.co.soramitsu.fearless_utils.runtime.AccountId
import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic
Expand Down Expand Up @@ -206,6 +210,51 @@ fun String.ethereumAddressToAccountId() = asEthereumAddress().toAccountId().valu
val SignerPayloadExtrinsic.chainId: String
get() = genesisHash.toHexString()

fun SignatureWrapperEcdsa(signature: ByteArray): SignatureWrapper.Ecdsa {
require(signature.size == 65)

val r = signature.copyOfRange(0, 32)
val s = signature.copyOfRange(32, 64)
val v = signature[64].ensureValidVByteFormat()

return SignatureWrapper.Ecdsa(v = byteArrayOf(v), r = r, s = s)
}

// Web3j supports only one format - when vByte is between [27..34]
// However, there is a second format - when vByte is between [0..7] - e.g. Ledger and Parity Signer
private fun Byte.ensureValidVByteFormat(): Byte {
if (this in 27..34) {
return this
}

if (this in 0..7) {
return (this + 27).toByte()
}

throw IllegalArgumentException("Invalid vByte: $this")
}

fun SignatureVerifier.verifyByAccountId(
signature: SignatureWrapper,
unHashedMessage: ByteArray,
expectedAccountId: AccountId,
messageHashing: Signer.MessageHashing
): Boolean {
return when(signature) {
// for Ed25519 and Sr25519 accountId == publicKey
is SignatureWrapper.Ed25519, is SignatureWrapper.Sr25519 -> {
verify(signature, messageHashing, unHashedMessage, publicKey = expectedAccountId)
}

is SignatureWrapper.Ecdsa -> {
val recoveredPublicKey = recoverSignaturePublicKey(unHashedMessage, signature, messageHashing)
val accountId = recoveredPublicKey.asEthereumPublicKey().toAccountId().value

accountId.contentEquals(expectedAccountId)
}
}
}

object Modules {
const val VESTING: String = "Vesting"
const val STAKING = "Staking"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,55 @@
package io.novafoundation.nova.feature_account_impl.data.repository

import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal
import jp.co.soramitsu.fearless_utils.runtime.AccountId
import io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.scan.ParitySignerAccount

interface ParitySignerRepository {

suspend fun addParitySignerWallet(
name: String,
substrateAccountId: AccountId,
paritySignerAccount: ParitySignerAccount,
): Long
}

class RealParitySignerRepository(
private val accountDao: MetaAccountDao
) : ParitySignerRepository {

override suspend fun addParitySignerWallet(name: String, substrateAccountId: AccountId): Long {
val metaAccount = MetaAccountLocal(
// it is safe to assume that accountId is equal to public key since Parity Signer only uses SR25519
substratePublicKey = substrateAccountId,
substrateAccountId = substrateAccountId,
substrateCryptoType = CryptoType.SR25519,
override suspend fun addParitySignerWallet(name: String, paritySignerAccount: ParitySignerAccount): Long {
val metaAccount = when (paritySignerAccount.accountType) {
ParitySignerAccount.Type.SUBSTRATE -> substrateMetaAccount(name, paritySignerAccount.accountId)
ParitySignerAccount.Type.ETHEREUM -> ethereumMetaAccount(name, paritySignerAccount.accountId)
}

return accountDao.insertMetaAccount(metaAccount)
}

private suspend fun substrateMetaAccount(name: String, accountId: ByteArray): MetaAccountLocal {
return MetaAccountLocal(
substratePublicKey = null,
substrateAccountId = accountId,
substrateCryptoType = null,
ethereumPublicKey = null,
ethereumAddress = null,
name = name,
isSelected = false,
position = accountDao.nextAccountPosition(),
type = MetaAccountLocal.Type.PARITY_SIGNER
)
}

return accountDao.insertMetaAccount(metaAccount)
private suspend fun ethereumMetaAccount(name: String, accountId: ByteArray): MetaAccountLocal {
return MetaAccountLocal(
substratePublicKey = null,
substrateAccountId = null,
substrateCryptoType = null,
ethereumPublicKey = null,
ethereumAddress = accountId,
name = name,
isSelected = false,
position = accountDao.nextAccountPosition(),
type = MetaAccountLocal.Type.PARITY_SIGNER
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.uos

import io.novafoundation.nova.core.model.CryptoType
import jp.co.soramitsu.fearless_utils.encrypt.EncryptionType
import jp.co.soramitsu.fearless_utils.encrypt.MultiChainEncryption

enum class ParitySignerUOSContentCode(override val value: Byte) : UOS.UOSPreludeValue {

Expand All @@ -12,12 +13,28 @@ enum class ParitySignerUOSPayloadCode(override val value: Byte) : UOS.UOSPrelude
TRANSACTION(0x02),
}

fun CryptoType.paritySignerUOSCryptoType(): UOS.UOSPreludeValue {
val byte: Byte = when (this) {
CryptoType.ED25519 -> 0x00
CryptoType.SR25519 -> 0x01
CryptoType.ECDSA -> 0x02
private const val ED25519_BYTE: Byte = 0x00
private const val SR25519_BYTE: Byte = 0x01
private const val ECDSA_BYTE: Byte = 0x02

fun MultiChainEncryption.paritySignerUOSCryptoType(): UOS.UOSPreludeValue {
val byte: Byte = when(this) {
MultiChainEncryption.Ethereum -> 0x03
is MultiChainEncryption.Substrate -> when (encryptionType) {
EncryptionType.ED25519 -> 0x00
EncryptionType.SR25519 -> 0x01
EncryptionType.ECDSA -> 0x02
}
}

return SimpleUOSPreludeValue(byte)
}

fun EncryptionType.Companion.fromParitySignerCryptoType(cryptoTypeByte: Byte): EncryptionType {
return when(cryptoTypeByte) {
ED25519_BYTE -> EncryptionType.ED25519
SR25519_BYTE -> EncryptionType.SR25519
ECDSA_BYTE -> EncryptionType.ECDSA
else -> throw IllegalArgumentException("Unknown crypto type byte: $cryptoTypeByte")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.

import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_impl.data.repository.ParitySignerRepository
import jp.co.soramitsu.fearless_utils.runtime.AccountId
import io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.scan.ParitySignerAccount
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

interface FinishImportParitySignerInteractor {

suspend fun createWallet(
name: String,
substrateAccountId: AccountId,
paritySignerAccount: ParitySignerAccount,
): Result<Unit>
}

Expand All @@ -19,9 +19,9 @@ class RealFinishImportParitySignerInteractor(
private val accountRepository: AccountRepository,
) : FinishImportParitySignerInteractor {

override suspend fun createWallet(name: String, substrateAccountId: AccountId): Result<Unit> = withContext(Dispatchers.Default) {
override suspend fun createWallet(name: String, paritySignerAccount: ParitySignerAccount): Result<Unit> = withContext(Dispatchers.Default) {
runCatching {
val metaId = repository.addParitySignerWallet(name, substrateAccountId)
val metaId = repository.addParitySignerWallet(name, paritySignerAccount)

accountRepository.selectMetaAccount(metaId)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
package io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.preview

import io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.scan.ParitySignerAccount
import io.novafoundation.nova.runtime.ext.defaultComparator
import io.novafoundation.nova.runtime.ext.isSubstrateBased
import io.novafoundation.nova.runtime.ext.type
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.first
import io.novafoundation.nova.runtime.multiNetwork.findChains

interface PreviewImportParitySignerInteractor {

suspend fun deriveSubstrateChainAccounts(accountId: ByteArray): List<ParitySignerAccountInChain>
suspend fun deriveSubstrateChainAccounts(account: ParitySignerAccount): List<ParitySignerAccountInChain>
}

class RealPreviewImportParitySignerInteractor(
private val chainRegistry: ChainRegistry
) : PreviewImportParitySignerInteractor {

override suspend fun deriveSubstrateChainAccounts(accountId: ByteArray): List<ParitySignerAccountInChain> {
val substrateChains = chainRegistry.currentChains.first().filter { it.isSubstrateBased }
override suspend fun deriveSubstrateChainAccounts(account: ParitySignerAccount): List<ParitySignerAccountInChain> {
val relevantChainType = account.chainType()
val relevantChains = chainRegistry.findChains { it.type == relevantChainType }

return substrateChains
return relevantChains
.sortedWith(Chain.defaultComparator())
.map { chain -> ParitySignerAccountInChain(chain, accountId) }
.map { chain -> ParitySignerAccountInChain(chain, account.accountId) }
}

private fun ParitySignerAccount.chainType() = when (accountType) {
ParitySignerAccount.Type.SUBSTRATE -> Chain.Type.SUBSTRATE
ParitySignerAccount.Type.ETHEREUM -> Chain.Type.ETHEREUM
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.scan

import io.novafoundation.nova.common.address.format.AddressFormat
import io.novafoundation.nova.common.address.format.AnySubstrateChainAddressFormat
import io.novafoundation.nova.common.address.format.EthereumAddressFormat
import jp.co.soramitsu.fearless_utils.runtime.AccountId

class ParitySignerAccount(
val accountId: AccountId,
val accountType: Type,
) {

enum class Type {
SUBSTRATE, ETHEREUM
}
}

val ParitySignerAccount.Type.addressFormat: AddressFormat
get() = when (this) {
ParitySignerAccount.Type.SUBSTRATE -> AnySubstrateChainAddressFormat()
ParitySignerAccount.Type.ETHEREUM -> EthereumAddressFormat()
}
Loading