mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-25 15:08:00 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl
|
||||
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
|
||||
interface ExternalSignRouter : ReturnableRouter {
|
||||
|
||||
fun openExtrinsicDetails(extrinsicContent: String)
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.data.evmApi
|
||||
|
||||
import io.novafoundation.nova.common.utils.toEcdsaSignatureData
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProvider
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource.UnknownChainOptions
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findEvmCallApi
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
|
||||
import okhttp3.OkHttpClient
|
||||
import org.web3j.crypto.RawTransaction
|
||||
import org.web3j.crypto.Sign.SignatureData
|
||||
import org.web3j.crypto.TransactionEncoder
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameterName
|
||||
import org.web3j.protocol.core.methods.request.Transaction
|
||||
import org.web3j.protocol.http.HttpService
|
||||
import org.web3j.rlp.RlpEncoder
|
||||
import org.web3j.rlp.RlpList
|
||||
import org.web3j.tx.RawTransactionManager
|
||||
import java.math.BigInteger
|
||||
|
||||
interface EvmApi {
|
||||
|
||||
suspend fun formTransaction(
|
||||
fromAddress: String,
|
||||
toAddress: String,
|
||||
data: String?,
|
||||
value: BigInteger?,
|
||||
nonce: BigInteger? = null,
|
||||
gasLimit: BigInteger? = null,
|
||||
gasPrice: BigInteger? = null,
|
||||
): RawTransaction
|
||||
|
||||
/**
|
||||
* @return hash of submitted transaction
|
||||
*/
|
||||
suspend fun sendTransaction(
|
||||
transaction: RawTransaction,
|
||||
signer: Signer,
|
||||
accountId: AccountId,
|
||||
ethereumChainId: Long,
|
||||
): String
|
||||
|
||||
/**
|
||||
* @return signed transaction, ready to be send by eth_sendRawTransaction
|
||||
*/
|
||||
suspend fun signTransaction(
|
||||
transaction: RawTransaction,
|
||||
signer: Signer,
|
||||
accountId: AccountId,
|
||||
ethereumChainId: Long,
|
||||
): String
|
||||
|
||||
suspend fun getAccountBalance(address: String): BigInteger
|
||||
|
||||
fun shutdown()
|
||||
}
|
||||
|
||||
class EvmApiFactory(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val gasPriceProviderFactory: GasPriceProviderFactory,
|
||||
) {
|
||||
|
||||
suspend fun create(chainSource: EvmChainSource): EvmApi? {
|
||||
val knownWeb3jApi = chainRegistry.findEvmCallApi(chainSource.evmChainId)
|
||||
val unknownChainOptions = chainSource.unknownChainOptions
|
||||
|
||||
return when {
|
||||
knownWeb3jApi != null -> {
|
||||
Web3JEvmApi(
|
||||
web3 = knownWeb3jApi,
|
||||
shouldShutdown = false,
|
||||
gasPriceProvider = gasPriceProviderFactory.create(knownWeb3jApi)
|
||||
)
|
||||
}
|
||||
|
||||
unknownChainOptions is UnknownChainOptions.WithFallBack -> {
|
||||
val web3Api = createWeb3j(unknownChainOptions.evmChain.rpcUrl)
|
||||
|
||||
Web3JEvmApi(
|
||||
web3 = web3Api,
|
||||
shouldShutdown = true,
|
||||
gasPriceProvider = gasPriceProviderFactory.create(web3Api)
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWeb3j(url: String): Web3j {
|
||||
return Web3j.build(HttpService(url, okHttpClient))
|
||||
}
|
||||
}
|
||||
|
||||
private class Web3JEvmApi(
|
||||
private val web3: Web3j,
|
||||
private val shouldShutdown: Boolean,
|
||||
private val gasPriceProvider: GasPriceProvider,
|
||||
) : EvmApi {
|
||||
|
||||
override suspend fun formTransaction(
|
||||
fromAddress: String,
|
||||
toAddress: String,
|
||||
data: String?,
|
||||
value: BigInteger?,
|
||||
nonce: BigInteger?,
|
||||
gasLimit: BigInteger?,
|
||||
gasPrice: BigInteger?,
|
||||
): RawTransaction {
|
||||
val finalNonce = nonce ?: getNonce(fromAddress)
|
||||
val finalGasPrice = gasPrice ?: gasPriceProvider.getGasPrice()
|
||||
|
||||
val dataOrDefault = data.orEmpty()
|
||||
|
||||
val finalGasLimit = gasLimit ?: run {
|
||||
val forFeeEstimatesTx = Transaction.createFunctionCallTransaction(
|
||||
fromAddress,
|
||||
finalNonce,
|
||||
null,
|
||||
null,
|
||||
toAddress,
|
||||
value,
|
||||
dataOrDefault
|
||||
)
|
||||
|
||||
estimateGasLimit(forFeeEstimatesTx)
|
||||
}
|
||||
|
||||
return RawTransaction.createTransaction(
|
||||
finalNonce,
|
||||
finalGasPrice,
|
||||
finalGasLimit,
|
||||
toAddress,
|
||||
value,
|
||||
dataOrDefault
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ethereum signing is adopted from [TransactionEncoder.signMessage] and [RawTransactionManager.sign]
|
||||
*/
|
||||
override suspend fun sendTransaction(
|
||||
transaction: RawTransaction,
|
||||
signer: Signer,
|
||||
accountId: AccountId,
|
||||
ethereumChainId: Long,
|
||||
): String {
|
||||
val signedRawTransaction = signTransaction(transaction, signer, accountId, ethereumChainId)
|
||||
|
||||
return sendTransaction(signedRawTransaction)
|
||||
}
|
||||
|
||||
override suspend fun signTransaction(
|
||||
transaction: RawTransaction,
|
||||
signer: Signer,
|
||||
accountId: AccountId,
|
||||
ethereumChainId: Long
|
||||
): String {
|
||||
val encodedTx = TransactionEncoder.encode(transaction, ethereumChainId)
|
||||
val signerPayload = SignerPayloadRaw(encodedTx, accountId)
|
||||
val signatureData = signer.signRaw(signerPayload).toEcdsaSignatureData()
|
||||
|
||||
val eip155SignatureData: SignatureData = TransactionEncoder.createEip155SignatureData(signatureData, ethereumChainId)
|
||||
|
||||
return transaction.encodeWith(eip155SignatureData).toHexString(withPrefix = true)
|
||||
}
|
||||
|
||||
override suspend fun getAccountBalance(address: String): BigInteger {
|
||||
return web3.ethGetBalance(address, DefaultBlockParameterName.LATEST).sendSuspend().balance
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
if (shouldShutdown) web3.shutdown()
|
||||
}
|
||||
|
||||
private suspend fun sendTransaction(transactionData: String): String {
|
||||
return web3.ethSendRawTransaction(transactionData).sendSuspend().transactionHash
|
||||
}
|
||||
|
||||
private suspend fun getNonce(address: String): BigInteger {
|
||||
return web3.ethGetTransactionCount(address, DefaultBlockParameterName.PENDING)
|
||||
.sendSuspend()
|
||||
.transactionCount
|
||||
}
|
||||
|
||||
private suspend fun estimateGasLimit(tx: Transaction): BigInteger {
|
||||
return web3.ethEstimateGas(tx).sendSuspend().amountUsed
|
||||
}
|
||||
|
||||
private fun RawTransaction.encodeWith(signatureData: SignatureData): ByteArray {
|
||||
val values = TransactionEncoder.asRlpValues(this, signatureData)
|
||||
val rlpList = RlpList(values)
|
||||
return RlpEncoder.encode(rlpList)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.di
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_external_sign_api.di.ExternalSignFeatureApi
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_impl.ExternalSignRouter
|
||||
import io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails.di.ExternalExtrinsicDetailsComponent
|
||||
import io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic.di.ExternalSignComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
ExternalSignFeatureDependencies::class
|
||||
],
|
||||
modules = [
|
||||
ExternalSignFeatureModule::class
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface ExternalSignFeatureComponent : ExternalSignFeatureApi {
|
||||
|
||||
fun signExtrinsicComponentFactory(): ExternalSignComponent.Factory
|
||||
|
||||
fun extrinsicDetailsComponentFactory(): ExternalExtrinsicDetailsComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: ExternalSignRouter,
|
||||
@BindsInstance signCommunicator: ExternalSignCommunicator,
|
||||
deps: ExternalSignFeatureDependencies
|
||||
): ExternalSignFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
AccountFeatureApi::class,
|
||||
WalletFeatureApi::class,
|
||||
RuntimeApi::class,
|
||||
CurrencyFeatureApi::class,
|
||||
]
|
||||
)
|
||||
interface ExternalSignFeatureDependenciesComponent : ExternalSignFeatureDependencies
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.di
|
||||
|
||||
import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
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.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.runtime.di.ExtrinsicSerialization
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
interface ExternalSignFeatureDependencies {
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
fun currencyRepository(): CurrencyRepository
|
||||
|
||||
fun accountRepository(): AccountRepository
|
||||
|
||||
fun resourceManager(): ResourceManager
|
||||
|
||||
fun selectedAccountUseCase(): SelectedAccountUseCase
|
||||
|
||||
fun addressIconGenerator(): AddressIconGenerator
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
fun imageLoader(): ImageLoader
|
||||
|
||||
fun extrinsicService(): ExtrinsicService
|
||||
|
||||
fun tokenRepository(): TokenRepository
|
||||
|
||||
fun secretStoreV2(): SecretStoreV2
|
||||
|
||||
@ExtrinsicSerialization
|
||||
fun extrinsicGson(): Gson
|
||||
|
||||
val feeLoaderMixinFactory: FeeLoaderMixinV2.Factory
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val okHttpClient: OkHttpClient
|
||||
|
||||
val walletRepository: WalletRepository
|
||||
|
||||
val validationExecutor: ValidationExecutor
|
||||
|
||||
val signerProvider: SignerProvider
|
||||
|
||||
val gasPriceProviderFactory: GasPriceProviderFactory
|
||||
|
||||
val rpcCalls: RpcCalls
|
||||
|
||||
val metadataShortenerService: MetadataShortenerService
|
||||
|
||||
val signingContextFactory: SigningContext.Factory
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_impl.ExternalSignRouter
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class ExternalSignFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val router: ExternalSignRouter,
|
||||
private val signCommunicator: ExternalSignCommunicator,
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val deps = DaggerExternalSignFeatureComponent_ExternalSignFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.currencyFeatureApi(getFeature(CurrencyFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerExternalSignFeatureComponent.factory()
|
||||
.create(router, signCommunicator, deps)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_external_sign_api.domain.sign.evm.EvmTypedMessageParser
|
||||
import io.novafoundation.nova.feature_external_sign_impl.di.modules.sign.EvmSignModule
|
||||
import io.novafoundation.nova.feature_external_sign_impl.di.modules.sign.PolkadotSignModule
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm.RealEvmTypedMessageParser
|
||||
|
||||
@Module(includes = [EvmSignModule::class, PolkadotSignModule::class])
|
||||
class ExternalSignFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideEvmTypedMessageParser(): EvmTypedMessageParser = RealEvmTypedMessageParser()
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.di.modules.sign
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_external_sign_impl.data.evmApi.EvmApiFactory
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm.EvmSignInteractorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.runtime.di.ExtrinsicSerialization
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@Module
|
||||
class EvmSignModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideEthereumApiFactory(
|
||||
okHttpClient: OkHttpClient,
|
||||
chainRegistry: ChainRegistry,
|
||||
gasPriceProviderFactory: GasPriceProviderFactory,
|
||||
): EvmApiFactory {
|
||||
return EvmApiFactory(okHttpClient, chainRegistry, gasPriceProviderFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSignInteractorFactory(
|
||||
chainRegistry: ChainRegistry,
|
||||
accountRepository: AccountRepository,
|
||||
tokenRepository: TokenRepository,
|
||||
@ExtrinsicSerialization extrinsicGson: Gson,
|
||||
addressIconGenerator: AddressIconGenerator,
|
||||
evmApiFactory: EvmApiFactory,
|
||||
signerProvider: SignerProvider,
|
||||
currencyRepository: CurrencyRepository
|
||||
) = EvmSignInteractorFactory(
|
||||
chainRegistry = chainRegistry,
|
||||
accountRepository = accountRepository,
|
||||
signerProvider = signerProvider,
|
||||
tokenRepository = tokenRepository,
|
||||
currencyRepository = currencyRepository,
|
||||
extrinsicGson = extrinsicGson,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
evmApiFactory = evmApiFactory
|
||||
)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.di.modules.sign
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
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.feature_external_sign_impl.di.modules.sign.PolkadotSignModule.BindsModule
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot.PolkadotSignInteractorFactory
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot.RealSignBytesChainResolver
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot.SignBytesChainResolver
|
||||
import io.novafoundation.nova.runtime.di.ExtrinsicSerialization
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [BindsModule::class])
|
||||
class PolkadotSignModule {
|
||||
|
||||
@Module
|
||||
interface BindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindSignBytesResolver(real: RealSignBytesChainResolver): SignBytesChainResolver
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSignInteractorFactory(
|
||||
extrinsicService: ExtrinsicService,
|
||||
chainRegistry: ChainRegistry,
|
||||
accountRepository: AccountRepository,
|
||||
@ExtrinsicSerialization extrinsicGson: Gson,
|
||||
addressIconGenerator: AddressIconGenerator,
|
||||
signerProvider: SignerProvider,
|
||||
metadataShortenerService: MetadataShortenerService,
|
||||
signingContextFactory: SigningContext.Factory,
|
||||
signBytesChainResolver: SignBytesChainResolver
|
||||
) = PolkadotSignInteractorFactory(
|
||||
extrinsicService = extrinsicService,
|
||||
chainRegistry = chainRegistry,
|
||||
accountRepository = accountRepository,
|
||||
extrinsicGson = extrinsicGson,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
signerProvider = signerProvider,
|
||||
metadataShortenerService = metadataShortenerService,
|
||||
signingContextFactory = signingContextFactory,
|
||||
signBytesChainResolver = signBytesChainResolver
|
||||
)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
|
||||
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_external_sign_api.model.signPayload.ExternalSignWallet
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
|
||||
|
||||
abstract class BaseExternalSignInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val wallet: ExternalSignWallet,
|
||||
private val signerProvider: SignerProvider,
|
||||
) : ExternalSignInteractor {
|
||||
|
||||
protected suspend fun resolveWalletSigner(): NovaSigner {
|
||||
val metaAccount = resolveMetaAccount()
|
||||
return signerProvider.rootSignerFor(metaAccount)
|
||||
}
|
||||
|
||||
protected suspend fun resolveMetaAccount(): MetaAccount {
|
||||
return when (wallet) {
|
||||
ExternalSignWallet.Current -> accountRepository.getSelectedMetaAccount()
|
||||
is ExternalSignWallet.WithId -> accountRepository.getMetaAccount(wallet.metaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign
|
||||
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
|
||||
fun String.tryConvertHexToUtf8(): String {
|
||||
return runCatching { fromHex().decodeToString(throwOnInvalidSequence = true) }
|
||||
.getOrDefault(this)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign
|
||||
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ExternalSignInteractor {
|
||||
|
||||
sealed class Error : Throwable() {
|
||||
class UnsupportedChain(val chainId: String) : Error()
|
||||
}
|
||||
|
||||
val validationSystem: ConfirmDAppOperationValidationSystem
|
||||
|
||||
suspend fun createAccountAddressModel(): AddressModel
|
||||
|
||||
suspend fun chainUi(): Result<ChainUi?>
|
||||
|
||||
fun utilityAssetFlow(): Flow<Chain.Asset>?
|
||||
|
||||
suspend fun calculateFee(): Fee?
|
||||
|
||||
suspend fun performOperation(upToDateFee: Fee?): ExternalSignCommunicator.Response?
|
||||
|
||||
suspend fun readableOperationContent(): String
|
||||
|
||||
suspend fun shutdown() {}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDetectedFailure
|
||||
|
||||
sealed class ConfirmDAppOperationValidationFailure {
|
||||
|
||||
class FeeSpikeDetected(override val payload: FeeChangeDetectedFailure.Payload<Fee>) :
|
||||
ConfirmDAppOperationValidationFailure(),
|
||||
FeeChangeDetectedFailure<Fee>
|
||||
}
|
||||
|
||||
data class ConfirmDAppOperationValidationPayload(
|
||||
val fee: Fee?
|
||||
)
|
||||
|
||||
typealias ConfirmDAppOperationValidationSystem = ValidationSystem<ConfirmDAppOperationValidationPayload, ConfirmDAppOperationValidationFailure>
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.asHexString
|
||||
import io.novafoundation.nova.common.utils.castOrNull
|
||||
import io.novafoundation.nova.common.utils.decodeEvmQuantity
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.invoke
|
||||
import io.novafoundation.nova.common.utils.lazyAsync
|
||||
import io.novafoundation.nova.common.utils.parseArbitraryObject
|
||||
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
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.SignerProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.failedSigningIfNotCancelled
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignWallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChain
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmPersonalSignMessage
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload.ConfirmTx
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload.PersonalSign
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload.SignTypedMessage
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTransaction
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTypedMessage
|
||||
import io.novafoundation.nova.feature_external_sign_impl.data.evmApi.EvmApi
|
||||
import io.novafoundation.nova.feature_external_sign_impl.data.evmApi.EvmApiFactory
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.BaseExternalSignInteractor
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationFailure
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationSystem
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.tryConvertHexToUtf8
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeChanges
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findEvmChain
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumAddress
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.extensions.toAccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.web3j.crypto.RawTransaction
|
||||
import org.web3j.crypto.TransactionDecoder
|
||||
|
||||
class EvmSignInteractorFactory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val currencyRepository: CurrencyRepository,
|
||||
private val extrinsicGson: Gson,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val evmApiFactory: EvmApiFactory,
|
||||
private val signerProvider: SignerProvider
|
||||
) {
|
||||
|
||||
fun create(request: ExternalSignRequest.Evm, wallet: ExternalSignWallet) = EvmSignInteractor(
|
||||
chainRegistry = chainRegistry,
|
||||
tokenRepository = tokenRepository,
|
||||
currencyRepository = currencyRepository,
|
||||
extrinsicGson = extrinsicGson,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
request = request,
|
||||
evmApiFactory = evmApiFactory,
|
||||
accountRepository = accountRepository,
|
||||
signerProvider = signerProvider,
|
||||
wallet = wallet
|
||||
)
|
||||
}
|
||||
|
||||
class EvmSignInteractor(
|
||||
private val evmApiFactory: EvmApiFactory,
|
||||
private val request: ExternalSignRequest.Evm,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val currencyRepository: CurrencyRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val extrinsicGson: Gson,
|
||||
private val signerProvider: SignerProvider,
|
||||
accountRepository: AccountRepository,
|
||||
wallet: ExternalSignWallet,
|
||||
) : BaseExternalSignInteractor(accountRepository, wallet, signerProvider),
|
||||
CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
|
||||
private val mostRecentFormedTx = singleReplaySharedFlow<RawTransaction>()
|
||||
|
||||
private val payload = request.payload
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val ethereumApi by GlobalScope.lazyAsync {
|
||||
payload.castOrNull<ConfirmTx>()?.chainSource?.let { chainSource ->
|
||||
evmApiFactory.create(chainSource)
|
||||
}
|
||||
}
|
||||
|
||||
override val validationSystem: ConfirmDAppOperationValidationSystem = ValidationSystem {
|
||||
if (payload is ConfirmTx) {
|
||||
checkForFeeChanges(
|
||||
calculateFee = { calculateFee()!! },
|
||||
currentFee = { it.fee },
|
||||
chainAsset = { it.fee!!.asset },
|
||||
error = ConfirmDAppOperationValidationFailure::FeeSpikeDetected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createAccountAddressModel(): AddressModel = withContext(Dispatchers.Default) {
|
||||
val address = request.payload.originAddress
|
||||
val someEthereumChain = chainRegistry.findChain { it.isEthereumBased }!! // always have at least one ethereum chain in the app
|
||||
|
||||
addressIconGenerator.createAccountAddressModel(someEthereumChain, address)
|
||||
}
|
||||
|
||||
override suspend fun chainUi(): Result<ChainUi?> = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
if (payload is ConfirmTx) {
|
||||
chainRegistry.findEvmChain(payload.chainSource.evmChainId)?.let(::mapChainToUi)
|
||||
?: payload.chainSource.fallbackChain?.let(::mapEvmChainToUi)
|
||||
?: throw ExternalSignInteractor.Error.UnsupportedChain(payload.chainSource.evmChainId.toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun utilityAssetFlow(): Flow<Chain.Asset>? {
|
||||
if (payload !is ConfirmTx) return null
|
||||
|
||||
return flowOf {
|
||||
chainRegistry.findEvmChain(payload.chainSource.evmChainId)?.utilityAsset
|
||||
?: createAssetFrom(payload.chainSource.unknownChainOptions)
|
||||
}.filterNotNull()
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(): Fee? = withContext(Dispatchers.Default) {
|
||||
if (payload !is ConfirmTx) return@withContext null
|
||||
|
||||
resolveWalletSigner()
|
||||
|
||||
val api = ethereumApi() ?: return@withContext null
|
||||
val chain = chainRegistry.findEvmChain(payload.chainSource.evmChainId)
|
||||
|
||||
// Commission asset for evm
|
||||
val chainAsset = chain?.utilityAsset ?: createAssetFrom(payload.chainSource.unknownChainOptions) ?: return@withContext null
|
||||
|
||||
val tx = api.formTransaction(payload.transaction, feeOverride = null)
|
||||
mostRecentFormedTx.emit(tx)
|
||||
|
||||
tx.fee(chainAsset)
|
||||
}
|
||||
|
||||
override suspend fun performOperation(upToDateFee: Fee?): ExternalSignCommunicator.Response? = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
when (payload) {
|
||||
is ConfirmTx -> confirmTx(payload.transaction, upToDateFee, payload.chainSource.evmChainId.toLong(), payload.action)
|
||||
is SignTypedMessage -> signTypedMessage(payload.message)
|
||||
is PersonalSign -> personalSign(payload.message)
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to sign evm tx", error)
|
||||
|
||||
error.failedSigningIfNotCancelled(request.id)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readableOperationContent(): String = withContext(Dispatchers.Default) {
|
||||
when (payload) {
|
||||
is ConfirmTx -> extrinsicGson.toJson(mostRecentFormedTx.first())
|
||||
is SignTypedMessage -> signTypedMessageReadableContent(payload)
|
||||
is PersonalSign -> personalSignReadableContent(payload)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shutdown() {
|
||||
ethereumApi()?.shutdown()
|
||||
}
|
||||
|
||||
private suspend fun confirmTx(basedOn: EvmTransaction, upToDateFee: Fee?, evmChainId: Long, action: ConfirmTx.Action): ExternalSignCommunicator.Response {
|
||||
val api = requireNotNull(ethereumApi())
|
||||
|
||||
val tx = api.formTransaction(basedOn, upToDateFee)
|
||||
|
||||
val originAccountId = originAccountId()
|
||||
val signer = resolveWalletSigner()
|
||||
|
||||
return when (action) {
|
||||
ConfirmTx.Action.SIGN -> {
|
||||
val signedTx = api.signTransaction(tx, signer, originAccountId, evmChainId)
|
||||
ExternalSignCommunicator.Response.Signed(request.id, signedTx)
|
||||
}
|
||||
|
||||
ConfirmTx.Action.SEND -> {
|
||||
val txHash = api.sendTransaction(tx, signer, originAccountId, evmChainId)
|
||||
ExternalSignCommunicator.Response.Sent(request.id, txHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signTypedMessage(message: EvmTypedMessage): ExternalSignCommunicator.Response.Signed {
|
||||
val signature = signMessage(message.data.fromHex())
|
||||
|
||||
return ExternalSignCommunicator.Response.Signed(request.id, signature)
|
||||
}
|
||||
|
||||
private suspend fun personalSign(message: EvmPersonalSignMessage): ExternalSignCommunicator.Response.Signed {
|
||||
val personalSignMessage = message.data.fromHex().asEthereumPersonalSignMessage()
|
||||
val payload = SignerPayloadRaw(personalSignMessage, originAccountId(), skipMessageHashing = true)
|
||||
|
||||
val signature = resolveWalletSigner().signRaw(payload).asHexString()
|
||||
|
||||
return ExternalSignCommunicator.Response.Signed(request.id, signature)
|
||||
}
|
||||
|
||||
private suspend fun signMessage(message: ByteArray): String {
|
||||
val signerPayload = SignerPayloadRaw(message, originAccountId(), skipMessageHashing = true)
|
||||
|
||||
return resolveWalletSigner().signRaw(signerPayload).asHexString()
|
||||
}
|
||||
|
||||
private fun personalSignReadableContent(payload: PersonalSign): String {
|
||||
val data = payload.message.data
|
||||
return data.tryConvertHexToUtf8()
|
||||
}
|
||||
|
||||
private fun signTypedMessageReadableContent(payload: SignTypedMessage): String {
|
||||
return runCatching {
|
||||
val parsedRaw = extrinsicGson.parseArbitraryObject(payload.message.raw!!)
|
||||
|
||||
val wrapped = mapOf(
|
||||
"data" to payload.message.data,
|
||||
"raw" to parsedRaw
|
||||
)
|
||||
|
||||
extrinsicGson.toJson(wrapped)
|
||||
}.getOrElse {
|
||||
extrinsicGson.toJson(payload.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapEvmChainToUi(metamaskChain: EvmChain): ChainUi {
|
||||
return ChainUi(
|
||||
id = metamaskChain.chainId,
|
||||
name = metamaskChain.chainName,
|
||||
icon = metamaskChain.iconUrl
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun EvmApi.formTransaction(basedOn: EvmTransaction, feeOverride: Fee?): RawTransaction {
|
||||
return when (basedOn) {
|
||||
is EvmTransaction.Raw -> TransactionDecoder.decode(basedOn.rawContent)
|
||||
|
||||
is EvmTransaction.Struct -> {
|
||||
val evmFee = feeOverride.castOrNull<EvmFee>()
|
||||
|
||||
formTransaction(
|
||||
fromAddress = basedOn.from,
|
||||
toAddress = basedOn.to,
|
||||
data = basedOn.data,
|
||||
value = basedOn.value?.decodeEvmQuantity(),
|
||||
nonce = basedOn.nonce?.decodeEvmQuantity(),
|
||||
gasLimit = evmFee?.gasLimit ?: basedOn.gas?.decodeEvmQuantity(),
|
||||
gasPrice = evmFee?.gasPrice ?: basedOn.gasPrice?.decodeEvmQuantity()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createTokenFrom(unknownChainOptions: EvmChainSource.UnknownChainOptions): Token? {
|
||||
if (unknownChainOptions !is EvmChainSource.UnknownChainOptions.WithFallBack) return null
|
||||
val asset = createAssetFrom(unknownChainOptions) ?: return null
|
||||
|
||||
return Token(
|
||||
configuration = asset,
|
||||
coinRate = null,
|
||||
currency = currencyRepository.getSelectedCurrency()
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAssetFrom(unknownChainOptions: EvmChainSource.UnknownChainOptions): Chain.Asset? {
|
||||
if (unknownChainOptions !is EvmChainSource.UnknownChainOptions.WithFallBack) return null
|
||||
|
||||
val evmChain = unknownChainOptions.evmChain
|
||||
val chainCurrency = evmChain.nativeCurrency
|
||||
|
||||
return Chain.Asset(
|
||||
icon = null,
|
||||
id = 0,
|
||||
priceId = null,
|
||||
chainId = evmChain.chainId,
|
||||
symbol = chainCurrency.symbol,
|
||||
precision = chainCurrency.decimals,
|
||||
buyProviders = emptyMap(),
|
||||
sellProviders = emptyMap(),
|
||||
staking = emptyList(),
|
||||
type = Chain.Asset.Type.EvmNative,
|
||||
name = chainCurrency.name,
|
||||
source = Chain.Asset.Source.ERC20,
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun RawTransaction.fee(chainAsset: Chain.Asset): Fee = EvmFee(
|
||||
gasLimit = gasLimit,
|
||||
gasPrice = gasPrice,
|
||||
submissionOrigin = submissionOrigin(),
|
||||
chainAsset
|
||||
)
|
||||
|
||||
private fun submissionOrigin() = SubmissionOrigin.singleOrigin(originAccountId())
|
||||
|
||||
private fun originAccountId() = payload.originAddress.asEthereumAddress().toAccountId().value
|
||||
|
||||
private val EvmChainSource.fallbackChain: EvmChain?
|
||||
get() = when (val options = unknownChainOptions) {
|
||||
|
||||
EvmChainSource.UnknownChainOptions.MustBeKnown -> null
|
||||
|
||||
is EvmChainSource.UnknownChainOptions.WithFallBack -> options.evmChain
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm
|
||||
|
||||
import org.web3j.crypto.Hash
|
||||
import org.web3j.crypto.Sign
|
||||
|
||||
private const val MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
|
||||
|
||||
private fun getEthereumMessagePrefix(messageLength: Int): ByteArray {
|
||||
return (MESSAGE_PREFIX + messageLength.toString()).encodeToByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adopted from [Sign.getEthereumMessageHash] since the former method is package-private and cannot be directly accessed by the calling code
|
||||
*/
|
||||
fun ByteArray.asEthereumPersonalSignMessage(): ByteArray {
|
||||
val prefix = getEthereumMessagePrefix(size)
|
||||
|
||||
val result = ByteArray(prefix.size + size)
|
||||
|
||||
System.arraycopy(prefix, 0, result, 0, prefix.size)
|
||||
System.arraycopy(this, 0, result, prefix.size, size)
|
||||
|
||||
return Hash.sha3(result)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm
|
||||
|
||||
import io.novafoundation.nova.feature_external_sign_api.domain.sign.evm.EvmTypedMessageParser
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTypedMessage
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import org.web3j.crypto.StructuredDataEncoder
|
||||
|
||||
internal class RealEvmTypedMessageParser : EvmTypedMessageParser {
|
||||
|
||||
override fun parseEvmTypedMessage(message: String): EvmTypedMessage {
|
||||
val encoder = StructuredDataEncoder(message)
|
||||
|
||||
return EvmTypedMessage(
|
||||
data = encoder.hashStructuredData().toHexString(withPrefix = true),
|
||||
raw = message
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import java.math.BigInteger
|
||||
|
||||
data class DAppParsedExtrinsic(
|
||||
val address: String,
|
||||
val nonce: BigInteger,
|
||||
val specVersion: Int,
|
||||
val transactionVersion: Int,
|
||||
val genesisHash: ByteArray,
|
||||
val era: Era,
|
||||
val blockHash: ByteArray,
|
||||
val tip: BigInteger,
|
||||
val metadataHash: ByteArray?,
|
||||
val call: GenericCall.Instance,
|
||||
val assetId: Any?,
|
||||
)
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.common.utils.asHexString
|
||||
import io.novafoundation.nova.common.utils.bigIntegerFromHex
|
||||
import io.novafoundation.nova.common.utils.convertToExternalCompatibleFormat
|
||||
import io.novafoundation.nova.common.utils.endsWith
|
||||
import io.novafoundation.nova.common.utils.intFromHex
|
||||
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
|
||||
import io.novafoundation.nova.common.utils.startsWith
|
||||
import io.novafoundation.nova.common.validation.EmptyValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.types.assetHub.decodeCustomTxPaymentId
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
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.SigningMode
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.setSignerData
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.signRaw
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.failedSigningIfNotCancelled
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignWallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.maybeSignExtrinsic
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.BaseExternalSignInteractor
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationSystem
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.tryConvertHexToUtf8
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.ext.anyAddressToAccountId
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.extrinsic.CustomTransactionExtensions
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.ChargeAssetTxPayment.Companion.chargeAssetTxPayment
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getChainOrNull
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
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.EraType
|
||||
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.signer.SignerPayloadRaw
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PolkadotSignInteractorFactory(
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val extrinsicGson: Gson,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val metadataShortenerService: MetadataShortenerService,
|
||||
private val signBytesChainResolver: SignBytesChainResolver,
|
||||
private val signerProvider: SignerProvider,
|
||||
private val signingContextFactory: SigningContext.Factory,
|
||||
) {
|
||||
|
||||
fun create(request: ExternalSignRequest.Polkadot, wallet: ExternalSignWallet) = PolkadotExternalSignInteractor(
|
||||
extrinsicService = extrinsicService,
|
||||
chainRegistry = chainRegistry,
|
||||
accountRepository = accountRepository,
|
||||
extrinsicGson = extrinsicGson,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
request = request,
|
||||
wallet = wallet,
|
||||
signerProvider = signerProvider,
|
||||
metadataShortenerService = metadataShortenerService,
|
||||
signingContextFactory = signingContextFactory,
|
||||
signBytesChainResolver = signBytesChainResolver
|
||||
)
|
||||
}
|
||||
|
||||
class PolkadotExternalSignInteractor(
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val extrinsicGson: Gson,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val request: ExternalSignRequest.Polkadot,
|
||||
private val signerProvider: SignerProvider,
|
||||
private val metadataShortenerService: MetadataShortenerService,
|
||||
private val signingContextFactory: SigningContext.Factory,
|
||||
private val signBytesChainResolver: SignBytesChainResolver,
|
||||
wallet: ExternalSignWallet,
|
||||
accountRepository: AccountRepository
|
||||
) : BaseExternalSignInteractor(accountRepository, wallet, signerProvider) {
|
||||
|
||||
private val signPayload = request.payload
|
||||
|
||||
private val actualParsedExtrinsic = singleReplaySharedFlow<DAppParsedExtrinsic>()
|
||||
|
||||
override val validationSystem: ConfirmDAppOperationValidationSystem = EmptyValidationSystem()
|
||||
|
||||
override suspend fun createAccountAddressModel(): AddressModel {
|
||||
val icon = addressIconGenerator.createAddressIcon(
|
||||
accountId = signPayload.accountId(),
|
||||
sizeInDp = AddressIconGenerator.SIZE_MEDIUM,
|
||||
backgroundColorRes = AddressIconGenerator.BACKGROUND_TRANSPARENT
|
||||
)
|
||||
|
||||
return AddressModel(signPayload.address, icon, name = null)
|
||||
}
|
||||
|
||||
override suspend fun chainUi(): Result<ChainUi?> {
|
||||
return runCatching {
|
||||
signPayload.maybeSignExtrinsic()?.let {
|
||||
mapChainToUi(it.chain())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun utilityAssetFlow(): Flow<Chain.Asset>? {
|
||||
val chainId = signPayload.maybeSignExtrinsic()?.genesisHash ?: return null
|
||||
|
||||
return flow {
|
||||
val chain = chainRegistry.getChainOrNull(chainId) ?: return@flow
|
||||
emit(chain.utilityAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performOperation(upToDateFee: Fee?): ExternalSignCommunicator.Response? = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
when (signPayload) {
|
||||
is PolkadotSignPayload.Json -> signExtrinsic(signPayload)
|
||||
is PolkadotSignPayload.Raw -> signBytes(signPayload)
|
||||
}
|
||||
}
|
||||
.onFailure { Log.e("PolkadotExternalSignInteractor", "Failed to sign", it) }
|
||||
.fold(
|
||||
onSuccess = { signedResult ->
|
||||
ExternalSignCommunicator.Response.Signed(request.id, signedResult.signature, signedResult.modifiedTransaction)
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.failedSigningIfNotCancelled(request.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun readableOperationContent(): String = withContext(Dispatchers.Default) {
|
||||
when (signPayload) {
|
||||
is PolkadotSignPayload.Json -> readableExtrinsicContent()
|
||||
is PolkadotSignPayload.Raw -> readableBytesContent(signPayload)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(): Fee? = withContext(Dispatchers.Default) {
|
||||
require(signPayload is PolkadotSignPayload.Json)
|
||||
|
||||
val chain = signPayload.chainOrNull() ?: return@withContext null
|
||||
|
||||
val signer = resolveWalletSigner()
|
||||
val (extrinsic, _, parsedExtrinsic) = signPayload.analyzeAndSign(signer, SigningMode.FEE)
|
||||
|
||||
actualParsedExtrinsic.emit(parsedExtrinsic)
|
||||
|
||||
extrinsicService.estimateFee(chain, extrinsic.extrinsicHex, signer)
|
||||
}
|
||||
|
||||
private fun readableBytesContent(signBytesPayload: PolkadotSignPayload.Raw): String {
|
||||
return signBytesPayload.data.tryConvertHexToUtf8()
|
||||
}
|
||||
|
||||
private suspend fun readableExtrinsicContent(): String {
|
||||
return extrinsicGson.toJson(actualParsedExtrinsic.first())
|
||||
}
|
||||
|
||||
private suspend fun signBytes(signBytesPayload: PolkadotSignPayload.Raw): SignedResult {
|
||||
val accountId = signBytesPayload.address.anyAddressToAccountId()
|
||||
|
||||
val signer = resolveWalletSigner()
|
||||
val payload = SignerPayloadRaw.fromUnsafeString(signBytesPayload.data, accountId)
|
||||
|
||||
val chainId = signBytesChainResolver.resolveChainId(signBytesPayload.address)
|
||||
val signature = signer.signRaw(payload, chainId).signatureWrapper.convertToExternalCompatibleFormat()
|
||||
|
||||
return SignedResult(signature.asHexString(), modifiedTransaction = null)
|
||||
}
|
||||
|
||||
private suspend fun signExtrinsic(extrinsicPayload: PolkadotSignPayload.Json): SignedResult {
|
||||
val signer = resolveWalletSigner()
|
||||
val (extrinsic, modifiedOriginal) = extrinsicPayload.analyzeAndSign(signer, SigningMode.SUBMISSION)
|
||||
|
||||
val modifiedTx = if (modifiedOriginal) extrinsic.extrinsicHex else null
|
||||
|
||||
return SignedResult(extrinsic.signatureHex, modifiedTx)
|
||||
}
|
||||
|
||||
private suspend fun PolkadotSignPayload.Json.analyzeAndSign(
|
||||
signer: NovaSigner,
|
||||
signingMode: SigningMode
|
||||
): ActualExtrinsic {
|
||||
val chain = chain()
|
||||
val runtime = chainRegistry.getRuntime(genesisHash)
|
||||
val parsedExtrinsic = parseDAppExtrinsic(runtime, this)
|
||||
|
||||
val actualMetadataHash = actualMetadataHash(chain, signer)
|
||||
|
||||
val signingContext = signingContextFactory.default(chain)
|
||||
|
||||
val extrinsic = with(parsedExtrinsic) {
|
||||
ExtrinsicBuilder(runtime, ExtrinsicVersion.V4, BatchMode.BATCH_ALL).apply {
|
||||
setTransactionExtension(CheckMortality(era, blockHash))
|
||||
setTransactionExtension(CheckGenesis(genesisHash))
|
||||
setTransactionExtension(ChargeTransactionPayment(tip))
|
||||
setTransactionExtension(CheckMetadataHash(actualMetadataHash.checkMetadataHash))
|
||||
setTransactionExtension(CheckSpecVersion(specVersion))
|
||||
setTransactionExtension(CheckTxVersion(transactionVersion))
|
||||
|
||||
call(parsedExtrinsic.call)
|
||||
CustomTransactionExtensions.applyDefaultValues(builder = this)
|
||||
applyCustomSignedExtensions(parsedExtrinsic)
|
||||
|
||||
signer.setSignerData(signingContext, signingMode)
|
||||
}
|
||||
}.buildExtrinsic()
|
||||
|
||||
val actualParsedExtrinsic = parsedExtrinsic.copy(
|
||||
metadataHash = actualMetadataHash.checkMetadataHash.metadataHash
|
||||
)
|
||||
|
||||
return ActualExtrinsic(
|
||||
signedExtrinsic = extrinsic,
|
||||
modifiedOriginal = actualMetadataHash.modifiedOriginal,
|
||||
actualParsedExtrinsic = actualParsedExtrinsic
|
||||
)
|
||||
}
|
||||
|
||||
private fun SignerPayloadRaw.Companion.fromUnsafeString(data: String, signer: AccountId): SignerPayloadRaw {
|
||||
val unsafeMessage = decodeSigningMessage(data)
|
||||
val safeMessage = protectSigningMessage(unsafeMessage)
|
||||
|
||||
return SignerPayloadRaw(safeMessage, signer)
|
||||
}
|
||||
|
||||
private fun decodeSigningMessage(data: String): ByteArray {
|
||||
return kotlin.runCatching { data.fromHex() }.getOrElse { data.encodeToByteArray() }
|
||||
}
|
||||
|
||||
private fun protectSigningMessage(message: ByteArray): ByteArray {
|
||||
val prefix = "<Bytes>".encodeToByteArray()
|
||||
val suffix = "</Bytes>".encodeToByteArray()
|
||||
|
||||
if (message.startsWith(prefix) && message.endsWith(suffix)) return message
|
||||
|
||||
return prefix + message + suffix
|
||||
}
|
||||
|
||||
private suspend fun PolkadotSignPayload.Json.actualMetadataHash(chain: Chain, signer: NovaSigner): ActualMetadataHash {
|
||||
// If a dapp haven't declared a permission to modify extrinsic - return whatever metadataHash present in payload
|
||||
if (withSignedTransaction != true) {
|
||||
return ActualMetadataHash(modifiedOriginal = false, hexHash = metadataHash)
|
||||
}
|
||||
|
||||
// If a dapp have specified metadata hash explicitly - use it
|
||||
if (metadataHash != null) {
|
||||
return ActualMetadataHash(modifiedOriginal = false, hexHash = metadataHash)
|
||||
}
|
||||
|
||||
// Else generate and use our own proof
|
||||
val metadataProof = metadataShortenerService.generateMetadataProof(chain.id)
|
||||
return ActualMetadataHash(modifiedOriginal = true, checkMetadataHash = metadataProof.checkMetadataHash)
|
||||
}
|
||||
|
||||
private fun PolkadotSignPayload.Json.decodedCall(runtime: RuntimeSnapshot): GenericCall.Instance {
|
||||
return GenericCall.fromHex(runtime, method)
|
||||
}
|
||||
|
||||
private suspend fun PolkadotSignPayload.Json.chain(): Chain {
|
||||
return chainRegistry.getChainOrNull(genesisHash) ?: throw ExternalSignInteractor.Error.UnsupportedChain(genesisHash)
|
||||
}
|
||||
|
||||
private suspend fun PolkadotSignPayload.Json.chainOrNull(): Chain? {
|
||||
return chainRegistry.getChainOrNull(genesisHash)
|
||||
}
|
||||
|
||||
private fun parseDAppExtrinsic(runtime: RuntimeSnapshot, payloadJSON: PolkadotSignPayload.Json): DAppParsedExtrinsic {
|
||||
return with(payloadJSON) {
|
||||
DAppParsedExtrinsic(
|
||||
address = address,
|
||||
nonce = nonce.bigIntegerFromHex(),
|
||||
specVersion = specVersion.intFromHex(),
|
||||
transactionVersion = transactionVersion.intFromHex(),
|
||||
genesisHash = genesisHash.fromHex(),
|
||||
blockHash = blockHash.fromHex(),
|
||||
era = EraType.fromHex(runtime, era),
|
||||
tip = tip.bigIntegerFromHex(),
|
||||
call = decodedCall(runtime),
|
||||
metadataHash = metadataHash?.fromHex(),
|
||||
assetId = payloadJSON.tryDecodeAssetId(runtime)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PolkadotSignPayload.accountId(): AccountId {
|
||||
return when (this) {
|
||||
is PolkadotSignPayload.Json -> {
|
||||
val chain = chainOrNull()
|
||||
|
||||
chain?.accountIdOf(address) ?: address.anyAddressToAccountId()
|
||||
}
|
||||
|
||||
is PolkadotSignPayload.Raw -> address.anyAddressToAccountId()
|
||||
}
|
||||
}
|
||||
|
||||
private class ActualMetadataHash(val modifiedOriginal: Boolean, val checkMetadataHash: CheckMetadataHashMode) {
|
||||
constructor(modifiedOriginal: Boolean, hash: ByteArray?) : this(modifiedOriginal, CheckMetadataHashMode(hash))
|
||||
|
||||
constructor(modifiedOriginal: Boolean, hexHash: String?) : this(modifiedOriginal, hexHash?.fromHex())
|
||||
}
|
||||
|
||||
private data class ActualExtrinsic(
|
||||
val signedExtrinsic: SendableExtrinsic,
|
||||
val modifiedOriginal: Boolean,
|
||||
val actualParsedExtrinsic: DAppParsedExtrinsic
|
||||
)
|
||||
|
||||
private data class SignedResult(val signature: String, val modifiedTransaction: String?)
|
||||
|
||||
private fun ExtrinsicBuilder.applyCustomSignedExtensions(parsedExtrinsic: DAppParsedExtrinsic): ExtrinsicBuilder {
|
||||
parsedExtrinsic.assetId?.let { chargeAssetTxPayment(it) }
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun PolkadotSignPayload.Json.tryDecodeAssetId(runtime: RuntimeSnapshot): Any? {
|
||||
return assetId?.let(runtime::decodeCustomTxPaymentId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CheckMetadataHashMode(hash: ByteArray?): CheckMetadataHashMode {
|
||||
return if (hash != null) {
|
||||
CheckMetadataHashMode.Enabled(hash)
|
||||
} else {
|
||||
CheckMetadataHashMode.Disabled
|
||||
}
|
||||
}
|
||||
|
||||
private val CheckMetadataHashMode.metadataHash: ByteArray?
|
||||
get() = if (this is CheckMetadataHashMode.Enabled) hash else null
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
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
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findChainIds
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.addressPrefix
|
||||
import javax.inject.Inject
|
||||
|
||||
interface SignBytesChainResolver {
|
||||
|
||||
suspend fun resolveChainId(address: String): ChainId?
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
class RealSignBytesChainResolver @Inject constructor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) : SignBytesChainResolver {
|
||||
|
||||
override suspend fun resolveChainId(address: String): ChainId? {
|
||||
return runCatching {
|
||||
val ss58Prefix = address.addressPrefix()
|
||||
detectChainIdFromSs58Prefix(ss58Prefix.toInt())
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun detectChainIdFromSs58Prefix(prefix: Int): ChainId? {
|
||||
val chains = chainRegistry.findChainIds { it.addressPrefix == prefix }
|
||||
|
||||
// This mapping is targeted to provide better detection for UAF and Polkadot Vault derivations (PV has Polkadot and Kusama derivations by default)
|
||||
return when {
|
||||
chains.isEmpty() -> null
|
||||
|
||||
Chain.Geneses.POLKADOT in chains -> Chain.Geneses.POLKADOT
|
||||
Chain.Geneses.KUSAMA in chains -> Chain.Geneses.KUSAMA
|
||||
|
||||
chains.size == 1 -> chains.single()
|
||||
|
||||
else -> Chain.Geneses.POLKADOT
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseBottomSheetFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.feature_external_sign_api.di.ExternalSignFeatureApi
|
||||
import io.novafoundation.nova.feature_external_sign_impl.databinding.FragmentDappExtrinsicDetailsBinding
|
||||
import io.novafoundation.nova.feature_external_sign_impl.di.ExternalSignFeatureComponent
|
||||
|
||||
class ExternalExtrinsicDetailsFragment : BaseBottomSheetFragment<ExternalExtrinsicDetailsViewModel, FragmentDappExtrinsicDetailsBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PAYLOAD_KEY = "PAYLOAD_KEY"
|
||||
|
||||
fun getBundle(extrinsicContent: String) = bundleOf(PAYLOAD_KEY to extrinsicContent)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentDappExtrinsicDetailsBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.signExtrinsicToolbar.setHomeButtonListener { viewModel.closeClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<ExternalSignFeatureComponent>(this, ExternalSignFeatureApi::class.java)
|
||||
.extrinsicDetailsComponentFactory()
|
||||
.create(this, argument(PAYLOAD_KEY))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: ExternalExtrinsicDetailsViewModel) {
|
||||
binder.extrinsicDetailsContent.text = viewModel.extrinsicContent
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.feature_external_sign_impl.ExternalSignRouter
|
||||
|
||||
class ExternalExtrinsicDetailsViewModel(
|
||||
private val router: ExternalSignRouter,
|
||||
val extrinsicContent: String
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun closeClicked() {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails.ExternalExtrinsicDetailsFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
ExternalExtrinsicDetailsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface ExternalExtrinsicDetailsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance extrinsicContent: String,
|
||||
): ExternalExtrinsicDetailsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: ExternalExtrinsicDetailsFragment)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_external_sign_impl.ExternalSignRouter
|
||||
import io.novafoundation.nova.feature_external_sign_impl.presentation.extrinsicDetails.ExternalExtrinsicDetailsViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class ExternalExtrinsicDetailsModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): ExternalExtrinsicDetailsViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(ExternalExtrinsicDetailsViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(ExternalExtrinsicDetailsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: ExternalSignRouter,
|
||||
extrinsicContent: String
|
||||
): ViewModel {
|
||||
return ExternalExtrinsicDetailsViewModel(
|
||||
router = router,
|
||||
extrinsicContent = extrinsicContent
|
||||
)
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.base.blockBackPressing
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.impl.observeValidations
|
||||
import io.novafoundation.nova.common.utils.makeGone
|
||||
import io.novafoundation.nova.common.utils.postToSelf
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.common.view.dialog.errorDialog
|
||||
import io.novafoundation.nova.common.view.setProgressState
|
||||
import io.novafoundation.nova.common.view.shape.addRipple
|
||||
import io.novafoundation.nova.common.view.shape.getBlockDrawable
|
||||
import io.novafoundation.nova.common.view.showValueOrHide
|
||||
import io.novafoundation.nova.feature_account_api.view.showWallet
|
||||
import io.novafoundation.nova.feature_account_api.view.showAddress
|
||||
import io.novafoundation.nova.feature_account_api.view.showChain
|
||||
import io.novafoundation.nova.feature_external_sign_api.di.ExternalSignFeatureApi
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
|
||||
import io.novafoundation.nova.feature_external_sign_impl.R
|
||||
import io.novafoundation.nova.feature_external_sign_impl.databinding.FragmentConfirmSignExtrinsicBinding
|
||||
import io.novafoundation.nova.feature_external_sign_impl.di.ExternalSignFeatureComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PAYLOAD_KEY = "DAppSignExtrinsicFragment.Payload"
|
||||
|
||||
class ExternalSignFragment : BaseFragment<ExternalSignViewModel, FragmentConfirmSignExtrinsicBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getBundle(payload: ExternalSignPayload) = bundleOf(PAYLOAD_KEY to payload)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentConfirmSignExtrinsicBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun initViews() {
|
||||
blockBackPressing()
|
||||
|
||||
binder.confirmDAppActionAllow.prepareForProgress(viewLifecycleOwner)
|
||||
|
||||
binder.confirmDAppActionAllow.setOnClickListener { viewModel.acceptClicked() }
|
||||
binder.confirmDAppActionAllow.setText(R.string.common_confirm)
|
||||
binder.confirmDAppActionReject.setOnClickListener { viewModel.rejectClicked() }
|
||||
|
||||
binder.confirmSignExtinsicDetails.setOnClickListener { viewModel.detailsClicked() }
|
||||
binder.confirmSignExtinsicDetails.background = with(requireContext()) {
|
||||
addRipple(getBlockDrawable())
|
||||
}
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<ExternalSignFeatureComponent>(this, ExternalSignFeatureApi::class.java)
|
||||
.signExtrinsicComponentFactory()
|
||||
.create(this, argument(PAYLOAD_KEY))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun subscribe(viewModel: ExternalSignViewModel) {
|
||||
setupFeeLoading(viewModel, binder.confirmSignExtinsicFee)
|
||||
observeValidations(viewModel)
|
||||
|
||||
viewModel.maybeChainUi.observe { chainUi ->
|
||||
binder.confirmSignExtinsicNetwork.postToSelf {
|
||||
if (chainUi != null) {
|
||||
showChain(chainUi)
|
||||
} else {
|
||||
makeGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.requestedAccountModel.observe {
|
||||
binder.confirmSignExtinsicAccount.postToSelf { showAddress(it) }
|
||||
}
|
||||
|
||||
viewModel.walletUi.observe {
|
||||
binder.confirmSignExtinsicWallet.postToSelf { showWallet(it) }
|
||||
}
|
||||
|
||||
binder.confirmSignExtinsicIcon.showDAppIcon(viewModel.dAppInfo?.icon, imageLoader)
|
||||
binder.confirmSignExtinsicDappUrl.showValueOrHide(viewModel.dAppInfo?.url)
|
||||
|
||||
viewModel.performingOperationInProgress.observe { operationInProgress ->
|
||||
val actionsAllowed = !operationInProgress
|
||||
|
||||
binder.confirmDAppActionReject.isEnabled = actionsAllowed
|
||||
binder.confirmDAppActionAllow.setProgressState(show = operationInProgress)
|
||||
}
|
||||
|
||||
viewModel.confirmUnrecoverableError.awaitableActionLiveData.observeEvent {
|
||||
errorDialog(
|
||||
context = requireContext(),
|
||||
onConfirm = { it.onSuccess(Unit) }
|
||||
) {
|
||||
setMessage(it.payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFeeLoading(viewModel: ExternalSignViewModel, feeView: FeeView) {
|
||||
val mixin = viewModel.originFeeMixin
|
||||
feeView.setVisible(mixin != null)
|
||||
|
||||
mixin?.let { setupFeeLoading(it, feeView) }
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.mixin.api.Validatable
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.validation.TransformedFailure
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.ValidationFlowActions
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.progressConsumer
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletModel
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignResponder
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignWallet
|
||||
import io.novafoundation.nova.feature_external_sign_impl.ExternalSignRouter
|
||||
import io.novafoundation.nova.feature_external_sign_impl.R
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationFailure
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationPayload
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.handleFeeSpikeDetected
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.PaymentCurrencySelectionMode
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.asFeeContextFromSelf
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitOptionalFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.createDefaultBy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private typealias SigningError = String
|
||||
|
||||
class ExternalSignViewModel(
|
||||
private val router: ExternalSignRouter,
|
||||
private val responder: ExternalSignResponder,
|
||||
private val interactor: ExternalSignInteractor,
|
||||
private val payload: ExternalSignPayload,
|
||||
private val validationExecutor: ValidationExecutor,
|
||||
private val resourceManager: ResourceManager,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
) : BaseViewModel(),
|
||||
Validatable by validationExecutor {
|
||||
|
||||
val confirmUnrecoverableError = actionAwaitableMixinFactory.confirmingAction<SigningError>()
|
||||
|
||||
private val commissionTokenFlow = interactor.utilityAssetFlow()
|
||||
?.shareInBackground()
|
||||
|
||||
val originFeeMixin = commissionTokenFlow?.let {
|
||||
feeLoaderMixinV2Factory.createDefaultBy(
|
||||
scope = viewModelScope,
|
||||
feeContext = it.asFeeContextFromSelf(),
|
||||
configuration = FeeLoaderMixinV2.Configuration(
|
||||
showZeroFiat = false,
|
||||
initialState = FeeLoaderMixinV2.Configuration.InitialState(
|
||||
paymentCurrencySelectionMode = PaymentCurrencySelectionMode.DETECT_FROM_FEE
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val _performingOperationInProgress = MutableStateFlow(false)
|
||||
val performingOperationInProgress: StateFlow<Boolean> = _performingOperationInProgress
|
||||
|
||||
val walletUi = walletUiUseCase.walletUiFor(payload.wallet)
|
||||
.shareInBackground()
|
||||
|
||||
val requestedAccountModel = flowOf {
|
||||
interactor.createAccountAddressModel()
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
val maybeChainUi = flowOf {
|
||||
interactor.chainUi()
|
||||
}
|
||||
.finishOnFailure()
|
||||
.shareInBackground()
|
||||
|
||||
val dAppInfo = payload.dappMetadata
|
||||
|
||||
init {
|
||||
maybeLoadFee()
|
||||
}
|
||||
|
||||
fun rejectClicked() {
|
||||
responder.respond(Response.Rejected(payload.signRequest.id))
|
||||
|
||||
exit()
|
||||
}
|
||||
|
||||
fun acceptClicked() = launch {
|
||||
_performingOperationInProgress.value = true
|
||||
|
||||
val validationPayload = ConfirmDAppOperationValidationPayload(
|
||||
fee = originFeeMixin?.awaitOptionalFee()
|
||||
)
|
||||
|
||||
validationExecutor.requireValid(
|
||||
validationSystem = interactor.validationSystem,
|
||||
payload = validationPayload,
|
||||
validationFailureTransformerCustom = ::validationFailureToUi,
|
||||
autoFixPayload = ::autoFixPayload,
|
||||
progressConsumer = _performingOperationInProgress.progressConsumer()
|
||||
) {
|
||||
performOperation(it.fee)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performOperation(upToDateFee: Fee?) = launch {
|
||||
interactor.performOperation(upToDateFee)?.let { response ->
|
||||
responder.respond(response)
|
||||
|
||||
exit()
|
||||
}
|
||||
|
||||
_performingOperationInProgress.value = false
|
||||
}
|
||||
|
||||
private fun maybeLoadFee() {
|
||||
originFeeMixin?.loadFee { interactor.calculateFee() }
|
||||
}
|
||||
|
||||
private suspend fun respondError(errorMessage: String?) = withContext(Dispatchers.Main) {
|
||||
val shouldPresent = if (errorMessage != null) {
|
||||
confirmUnrecoverableError.awaitAction(errorMessage)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
val response = Response.SigningFailed(payload.signRequest.id, shouldPresent)
|
||||
|
||||
responder.respond(response)
|
||||
exit()
|
||||
}
|
||||
|
||||
private suspend fun respondError(error: Throwable) {
|
||||
val errorMessage = when (error) {
|
||||
is ExternalSignInteractor.Error.UnsupportedChain -> resourceManager.getString(R.string.dapp_sign_error_unsupported_chain, error.chainId)
|
||||
else -> null
|
||||
}
|
||||
|
||||
respondError(errorMessage)
|
||||
}
|
||||
|
||||
fun detailsClicked() {
|
||||
launch {
|
||||
val extrinsicContent = interactor.readableOperationContent()
|
||||
|
||||
router.openExtrinsicDetails(extrinsicContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun exit() = launch {
|
||||
interactor.shutdown()
|
||||
|
||||
router.back()
|
||||
}
|
||||
|
||||
private fun validationFailureToUi(
|
||||
failure: ValidationStatus.NotValid<ConfirmDAppOperationValidationFailure>,
|
||||
actions: ValidationFlowActions<*>
|
||||
): TransformedFailure? {
|
||||
return when (val reason = failure.reason) {
|
||||
is ConfirmDAppOperationValidationFailure.FeeSpikeDetected -> originFeeMixin?.let {
|
||||
handleFeeSpikeDetected(
|
||||
error = reason,
|
||||
resourceManager = resourceManager,
|
||||
setFee = originFeeMixin,
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun autoFixPayload(
|
||||
payload: ConfirmDAppOperationValidationPayload,
|
||||
failure: ConfirmDAppOperationValidationFailure
|
||||
): ConfirmDAppOperationValidationPayload {
|
||||
return when (failure) {
|
||||
is ConfirmDAppOperationValidationFailure.FeeSpikeDetected -> payload.copy(fee = failure.payload.newFee)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Flow<Result<T>>.finishOnFailure(): Flow<T?> {
|
||||
return onEach { result -> result.onFailure { respondError(it) } }
|
||||
.map { it.getOrNull() }
|
||||
}
|
||||
|
||||
private fun WalletUiUseCase.walletUiFor(externalSignWallet: ExternalSignWallet): Flow<WalletModel> {
|
||||
return when (externalSignWallet) {
|
||||
ExternalSignWallet.Current -> selectedWalletUiFlow(showAddressIcon = true)
|
||||
is ExternalSignWallet.WithId -> walletUiFlow(externalSignWallet.metaId, showAddressIcon = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic.ExternalSignFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
ExternalSignModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface ExternalSignComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: ExternalSignPayload,
|
||||
): ExternalSignComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: ExternalSignFragment)
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_impl.ExternalSignRouter
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm.EvmSignInteractorFactory
|
||||
import io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot.PolkadotSignInteractorFactory
|
||||
import io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic.ExternalSignViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class ExternalSignModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
polkadotSignInteractorFactory: PolkadotSignInteractorFactory,
|
||||
metamaskSignInteractorFactory: EvmSignInteractorFactory,
|
||||
payload: ExternalSignPayload
|
||||
): ExternalSignInteractor = when (val request = payload.signRequest) {
|
||||
is ExternalSignRequest.Polkadot -> polkadotSignInteractorFactory.create(request, payload.wallet)
|
||||
is ExternalSignRequest.Evm -> metamaskSignInteractorFactory.create(request, payload.wallet)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): ExternalSignViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(ExternalSignViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(ExternalSignViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: ExternalSignRouter,
|
||||
feeLoaderMixinFactory: FeeLoaderMixinV2.Factory,
|
||||
interactor: ExternalSignInteractor,
|
||||
payload: ExternalSignPayload,
|
||||
communicator: ExternalSignCommunicator,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
validationExecutor: ValidationExecutor,
|
||||
resourceManager: ResourceManager,
|
||||
actionAwaitableMixin: ActionAwaitableMixin.Factory
|
||||
): ViewModel {
|
||||
return ExternalSignViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
feeLoaderMixinV2Factory = feeLoaderMixinFactory,
|
||||
payload = payload,
|
||||
responder = communicator,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
validationExecutor = validationExecutor,
|
||||
resourceManager = resourceManager,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixin
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/confirmSignExtinsicToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:homeButtonVisible="false"
|
||||
app:titleText="@string/common_sign_request" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/confirmSignExtinsicIcon"
|
||||
style="@style/Widget.Nova.Icon.Big"
|
||||
android:layout_marginTop="24dp"
|
||||
tools:src="@drawable/ic_earth" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/confirmSignExtinsicTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Title3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/common_confirm_title"
|
||||
android:textColor="@color/text_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/confirmSignExtinsicSubTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/dapp_confirm_sign_extrinsic_subtitle"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmSignExtinsicDappUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/dapp_dapp" />
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmSignExtinsicWallet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/tabbar_wallet_title" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmSignExtinsicAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_account_address" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmSignExtinsicNetwork"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_network" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
android:id="@+id/confirmSignExtinsicFee"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/network_fee" />
|
||||
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.GoNextView
|
||||
android:id="@+id/confirmSignExtinsicDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/transaction_details_title"
|
||||
android:textAppearance="@style/GoNextTransactionDetailsTextAppearance"
|
||||
app:actionIcon="@drawable/ic_chevron_right"
|
||||
app:dividerVisible="false"
|
||||
tools:background="@color/block_background" />
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/confirmDAppActionReject"
|
||||
style="@style/Widget.Nova.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/common_reject"
|
||||
android:theme="@style/NegativeAccent"
|
||||
app:appearance="secondary" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/confirmDAppActionAllow"
|
||||
style="@style/Widget.Nova.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/common_allow"
|
||||
android:theme="@style/AccentBlue" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/signExtrinsicContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/signExtrinsicToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:contentBackground="@android:color/transparent"
|
||||
app:dividerVisible="false"
|
||||
app:homeButtonIcon="@drawable/ic_close"
|
||||
app:titleText="@string/transaction_details_title" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/dapp_sign_extrinsic_details_subtitle"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/extrinsicDetailsContent"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Monospace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/extrinsic_details_background"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="@tools:sample/lorem[200]" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_external_sign_impl.domain.sign.evm
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
import org.web3j.crypto.Sign
|
||||
|
||||
class PersonalSignKtTest {
|
||||
|
||||
@Test
|
||||
fun `should perform personal sign`() {
|
||||
val message = "Test".encodeToByteArray()
|
||||
val expected = expectedSign(message)
|
||||
val actual = message.asEthereumPersonalSignMessage()
|
||||
|
||||
assertArrayEquals(expected, actual)
|
||||
}
|
||||
|
||||
// call to reference package-private implementation via reflection.
|
||||
// It is not good approach to use this as our actual implementation since reflection is unsafe and slow
|
||||
private fun expectedSign(message: ByteArray): ByteArray {
|
||||
val method = Sign::class.java.getDeclaredMethod("getEthereumMessageHash", ByteArray::class.java)
|
||||
method.isAccessible = true
|
||||
|
||||
return method.invoke(null, message) as ByteArray
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user