mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-25 04:37:59 +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>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl
|
||||
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.WalletConnectSessionDetailsPayload
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.WalletConnectSessionsPayload
|
||||
|
||||
interface WalletConnectRouter : ReturnableRouter {
|
||||
|
||||
fun openSessionDetails(payload: WalletConnectSessionDetailsPayload)
|
||||
|
||||
fun openScanPairingQrCode()
|
||||
|
||||
fun backToSettings()
|
||||
|
||||
fun openWalletConnectSessions(payload: WalletConnectSessionsPayload)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.data.model.evm
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class WalletConnectEvmTransaction(
|
||||
val from: String,
|
||||
val to: String,
|
||||
val data: String?,
|
||||
val nonce: String?,
|
||||
val gasPrice: String?,
|
||||
@SerializedName("gasLimit", alternate = ["gas"])
|
||||
val gasLimit: String?,
|
||||
val value: String?,
|
||||
)
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.core_db.dao.WalletConnectSessionsDao
|
||||
import io.novafoundation.nova.core_db.model.WalletConnectPairingLocal
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectPairingAccount
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletConnectPairingRepository {
|
||||
|
||||
suspend fun addPairingAccount(pairingAccount: WalletConnectPairingAccount)
|
||||
|
||||
suspend fun getPairingAccount(pairingTopic: String): WalletConnectPairingAccount?
|
||||
|
||||
fun allPairingAccountsFlow(): Flow<List<WalletConnectPairingAccount>>
|
||||
|
||||
fun pairingAccountsByMetaIdFlow(metaId: Long): Flow<List<WalletConnectPairingAccount>>
|
||||
|
||||
suspend fun removeAllPairingsOtherThan(activePairingTopics: List<String>)
|
||||
}
|
||||
|
||||
class RealWalletConnectPairingRepository(
|
||||
private val dao: WalletConnectSessionsDao,
|
||||
) : WalletConnectPairingRepository {
|
||||
|
||||
override suspend fun addPairingAccount(pairingAccount: WalletConnectPairingAccount) {
|
||||
dao.insertPairing(mapSessionToLocal(pairingAccount))
|
||||
}
|
||||
|
||||
override suspend fun getPairingAccount(pairingTopic: String): WalletConnectPairingAccount? {
|
||||
return dao.getPairing(pairingTopic)?.let(::mapSessionFromLocal)
|
||||
}
|
||||
|
||||
override fun allPairingAccountsFlow(): Flow<List<WalletConnectPairingAccount>> {
|
||||
return dao.allPairingsFlow().mapList(::mapSessionFromLocal)
|
||||
}
|
||||
|
||||
override fun pairingAccountsByMetaIdFlow(metaId: Long): Flow<List<WalletConnectPairingAccount>> {
|
||||
return dao.pairingsByMetaIdFlow(metaId).mapList(::mapSessionFromLocal)
|
||||
}
|
||||
|
||||
override suspend fun removeAllPairingsOtherThan(activePairingTopics: List<String>) {
|
||||
dao.removeAllPairingsOtherThan(activePairingTopics)
|
||||
}
|
||||
|
||||
private fun mapSessionToLocal(session: WalletConnectPairingAccount): WalletConnectPairingLocal {
|
||||
return with(session) {
|
||||
WalletConnectPairingLocal(pairingTopic = pairingTopic, metaId = metaId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapSessionFromLocal(sessionLocal: WalletConnectPairingLocal): WalletConnectPairingAccount {
|
||||
return with(sessionLocal) {
|
||||
WalletConnectPairingAccount(pairingTopic = pairingTopic, metaId = metaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.data.repository
|
||||
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.Session
|
||||
import io.novafoundation.nova.common.utils.added
|
||||
import io.novafoundation.nova.common.utils.removed
|
||||
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface WalletConnectSessionRepository {
|
||||
|
||||
suspend fun setSessions(sessions: List<Session>)
|
||||
|
||||
suspend fun getSession(sessionTopic: String): Session?
|
||||
|
||||
fun allSessionsFlow(): Flow<List<Session>>
|
||||
|
||||
fun sessionFlow(sessionTopic: String): Flow<Session?>
|
||||
|
||||
suspend fun addSession(session: Session)
|
||||
|
||||
suspend fun removeSession(sessionTopic: String)
|
||||
|
||||
fun numberOfSessionsFlow(): Flow<Int>
|
||||
|
||||
suspend fun numberOfSessionAccounts(): Int
|
||||
|
||||
fun numberOfSessionsFlow(pairingTopics: Set<String>): Flow<Int>
|
||||
}
|
||||
|
||||
class InMemoryWalletConnectSessionRepository : WalletConnectSessionRepository {
|
||||
|
||||
private val state = singleReplaySharedFlow<List<Session>>()
|
||||
|
||||
override suspend fun setSessions(sessions: List<Session>) {
|
||||
state.emit(sessions)
|
||||
}
|
||||
|
||||
override suspend fun getSession(sessionTopic: String): Session? {
|
||||
return state.first().find { it.topic == sessionTopic }
|
||||
}
|
||||
|
||||
override fun allSessionsFlow(): Flow<List<Session>> {
|
||||
return state
|
||||
}
|
||||
|
||||
override fun sessionFlow(sessionTopic: String): Flow<Session?> {
|
||||
return state.map { allSessions -> allSessions.find { it.topic == sessionTopic } }
|
||||
}
|
||||
|
||||
override suspend fun addSession(session: Session) {
|
||||
modifyState { current ->
|
||||
current.added(session)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeSession(sessionTopic: String) {
|
||||
modifyState { current ->
|
||||
current.removed { it.topic == sessionTopic }
|
||||
}
|
||||
}
|
||||
|
||||
override fun numberOfSessionsFlow(): Flow<Int> {
|
||||
return state.map { it.size }
|
||||
}
|
||||
|
||||
override fun numberOfSessionsFlow(pairingTopics: Set<String>): Flow<Int> {
|
||||
return state.map { sessions ->
|
||||
sessions.filter { it.pairingTopic in pairingTopics }.size
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun numberOfSessionAccounts(): Int {
|
||||
return state.first().size
|
||||
}
|
||||
|
||||
private suspend fun modifyState(modify: (List<Session>) -> List<Session>) {
|
||||
state.emit(modify(state.first()))
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.di
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.caip.di.CaipApi
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
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_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.scan.di.WalletConnectScanComponent
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.ApproveSessionCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.di.WalletConnectApproveSessionComponent
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.di.WalletConnectSessionDetailsComponent
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.di.WalletConnectSessionsComponent
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
WalletConnectFeatureDependencies::class
|
||||
],
|
||||
modules = [
|
||||
WalletConnectFeatureModule::class
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface WalletConnectFeatureComponent : WalletConnectFeatureApi {
|
||||
|
||||
fun walletConnectSessionsComponentFactory(): WalletConnectSessionsComponent.Factory
|
||||
|
||||
fun walletConnectSessionDetailsComponentFactory(): WalletConnectSessionDetailsComponent.Factory
|
||||
|
||||
fun walletConnectApproveSessionComponentFactory(): WalletConnectApproveSessionComponent.Factory
|
||||
|
||||
fun walletConnectScanComponentFactory(): WalletConnectScanComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: WalletConnectRouter,
|
||||
@BindsInstance signCommunicator: ExternalSignCommunicator,
|
||||
@BindsInstance approveSessionCommunicator: ApproveSessionCommunicator,
|
||||
deps: WalletConnectFeatureDependencies
|
||||
): WalletConnectFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
DbApi::class,
|
||||
AccountFeatureApi::class,
|
||||
RuntimeApi::class,
|
||||
CaipApi::class,
|
||||
ExternalSignFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface WalletConnectFeatureDependenciesComponent : WalletConnectFeatureDependencies
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.di
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Parser
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Resolver
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.coroutines.RootScope
|
||||
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.core_db.dao.WalletConnectSessionsDao
|
||||
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_account_api.presenatation.mixin.selectWallet.SelectWalletMixin
|
||||
import io.novafoundation.nova.feature_external_sign_api.domain.sign.evm.EvmTypedMessageParser
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
interface WalletConnectFeatureDependencies {
|
||||
|
||||
val accountRepository: AccountRepository
|
||||
|
||||
val resourceManager: ResourceManager
|
||||
|
||||
val selectedAccountUseCase: SelectedAccountUseCase
|
||||
|
||||
val addressIconGenerator: AddressIconGenerator
|
||||
|
||||
val gson: Gson
|
||||
|
||||
val chainRegistry: ChainRegistry
|
||||
|
||||
val imageLoader: ImageLoader
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val permissionsAskerFactory: PermissionsAskerFactory
|
||||
|
||||
val caip2Resolver: Caip2Resolver
|
||||
|
||||
val caip2Parser: Caip2Parser
|
||||
|
||||
val evmTypedMessageParser: EvmTypedMessageParser
|
||||
|
||||
val sessionsDao: WalletConnectSessionsDao
|
||||
|
||||
val selectWalletMixinFactory: SelectWalletMixin.Factory
|
||||
|
||||
val appContext: Context
|
||||
|
||||
val automaticInteractionGate: AutomaticInteractionGate
|
||||
|
||||
fun rootScope(): RootScope
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.di
|
||||
|
||||
import io.novafoundation.nova.caip.di.CaipApi
|
||||
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.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
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_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.ApproveSessionCommunicator
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class WalletConnectFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val router: WalletConnectRouter,
|
||||
private val signCommunicator: ExternalSignCommunicator,
|
||||
private val approveSessionCommunicator: ApproveSessionCommunicator,
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val deps = DaggerWalletConnectFeatureComponent_WalletConnectFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.caipApi(getFeature(CaipApi::class.java))
|
||||
.externalSignFeatureApi(getFeature(ExternalSignFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerWalletConnectFeatureComponent.factory()
|
||||
.create(router, signCommunicator, approveSessionCommunicator, deps)
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.di
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Parser
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Resolver
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.coroutines.RootScope
|
||||
import io.novafoundation.nova.core_db.dao.WalletConnectSessionsDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_external_sign_api.domain.sign.evm.EvmTypedMessageParser
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.InMemoryWalletConnectSessionRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.RealWalletConnectPairingRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.WalletConnectPairingRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.WalletConnectSessionRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.di.deeplinks.DeepLinkModule
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.RealWalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.RealWalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.CompoundWalletConnectRequestFactory
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.WalletConnectRequest
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm.EvmWalletConnectRequestFactory
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.polkadot.PolkadotWalletConnectRequestFactory
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.service.RealWalletConnectService
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.ApproveSessionCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common.RealWalletConnectSessionMapper
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common.WalletConnectSessionMapper
|
||||
|
||||
@Module(includes = [DeepLinkModule::class])
|
||||
class WalletConnectFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePolkadotRequestFactory(
|
||||
gson: Gson,
|
||||
caip2Parser: Caip2Parser,
|
||||
appContext: Context
|
||||
): PolkadotWalletConnectRequestFactory {
|
||||
return PolkadotWalletConnectRequestFactory(gson, caip2Parser, appContext)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideEvmRequestFactory(
|
||||
gson: Gson,
|
||||
caip2Parser: Caip2Parser,
|
||||
typedMessageParser: EvmTypedMessageParser,
|
||||
appContext: Context
|
||||
): EvmWalletConnectRequestFactory {
|
||||
return EvmWalletConnectRequestFactory(gson, caip2Parser, typedMessageParser, appContext)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideRequestFactory(
|
||||
polkadotFactory: PolkadotWalletConnectRequestFactory,
|
||||
evmFactory: EvmWalletConnectRequestFactory,
|
||||
): WalletConnectRequest.Factory {
|
||||
return CompoundWalletConnectRequestFactory(polkadotFactory, evmFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePairingRepository(dao: WalletConnectSessionsDao): WalletConnectPairingRepository = RealWalletConnectPairingRepository(dao)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSessionRepository(): WalletConnectSessionRepository = InMemoryWalletConnectSessionRepository()
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideInteractor(
|
||||
caip2Resolver: Caip2Resolver,
|
||||
requestFactory: WalletConnectRequest.Factory,
|
||||
walletConnectPairingRepository: WalletConnectPairingRepository,
|
||||
walletConnectSessionRepository: WalletConnectSessionRepository,
|
||||
accountRepository: AccountRepository,
|
||||
caip2Parser: Caip2Parser
|
||||
): WalletConnectSessionInteractor = RealWalletConnectSessionInteractor(
|
||||
caip2Resolver = caip2Resolver,
|
||||
walletConnectRequestFactory = requestFactory,
|
||||
walletConnectPairingRepository = walletConnectPairingRepository,
|
||||
accountRepository = accountRepository,
|
||||
caip2Parser = caip2Parser,
|
||||
walletConnectSessionRepository = walletConnectSessionRepository
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideWalletConnectService(
|
||||
rootScope: RootScope,
|
||||
interactor: WalletConnectSessionInteractor,
|
||||
dAppSignRequester: ExternalSignCommunicator,
|
||||
approveSessionCommunicator: ApproveSessionCommunicator,
|
||||
): WalletConnectService {
|
||||
return RealWalletConnectService(
|
||||
parentScope = rootScope,
|
||||
interactor = interactor,
|
||||
dAppSignRequester = dAppSignRequester,
|
||||
approveSessionRequester = approveSessionCommunicator
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSessionMapper(resourceManager: ResourceManager): WalletConnectSessionMapper {
|
||||
return RealWalletConnectSessionMapper(resourceManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSessionUseCase(
|
||||
pairingRepository: WalletConnectPairingRepository,
|
||||
sessionRepository: WalletConnectSessionRepository
|
||||
): WalletConnectSessionsUseCase {
|
||||
return RealWalletConnectSessionsUseCase(pairingRepository, sessionRepository)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.di.deeplinks
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.deeplinks.WalletConnectDeepLinks
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.deeplink.WalletConnectPairDeeplinkHandler
|
||||
|
||||
@Module
|
||||
class DeepLinkModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideWalletConnectDeepLinkHandler(
|
||||
walletConnectService: WalletConnectService,
|
||||
automaticInteractionGate: AutomaticInteractionGate
|
||||
) = WalletConnectPairDeeplinkHandler(
|
||||
walletConnectService,
|
||||
automaticInteractionGate
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinks(buyCallback: WalletConnectPairDeeplinkHandler): WalletConnectDeepLinks {
|
||||
return WalletConnectDeepLinks(listOf(buyCallback))
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.model
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class SessionChains(
|
||||
val required: ResolvedChains,
|
||||
val optional: ResolvedChains,
|
||||
) {
|
||||
|
||||
class ResolvedChains(val knownChains: Set<Chain>, val unknownChains: Set<String>)
|
||||
}
|
||||
|
||||
fun SessionChains.allKnownChains(): Set<Chain> {
|
||||
return required.knownChains + optional.knownChains
|
||||
}
|
||||
|
||||
fun SessionChains.allUnknownChains(): Set<String> {
|
||||
return required.unknownChains + optional.unknownChains
|
||||
}
|
||||
|
||||
fun SessionChains.ResolvedChains.hasUnknown(): Boolean {
|
||||
return unknownChains.isNotEmpty()
|
||||
}
|
||||
|
||||
fun SessionChains.ResolvedChains.hasKnown(): Boolean {
|
||||
return knownChains.isNotEmpty()
|
||||
}
|
||||
|
||||
fun SessionChains.ResolvedChains.hasAny(): Boolean {
|
||||
return hasUnknown() || hasKnown()
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.model
|
||||
|
||||
class WalletConnectPairingAccount(
|
||||
val metaId: Long,
|
||||
val pairingTopic: String
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class WalletConnectSession(
|
||||
val connectedMetaAccount: MetaAccount,
|
||||
val dappMetadata: SessionDappMetadata?,
|
||||
val sessionTopic: String,
|
||||
)
|
||||
|
||||
class SessionDappMetadata(
|
||||
val dAppUrl: String,
|
||||
val icon: String?,
|
||||
val name: String?
|
||||
)
|
||||
|
||||
val SessionDappMetadata.dAppTitle: String
|
||||
get() = name ?: dAppUrl
|
||||
|
||||
class WalletConnectSessionDetails(
|
||||
val connectedMetaAccount: MetaAccount,
|
||||
val dappMetadata: SessionDappMetadata?,
|
||||
val chains: Set<Chain>,
|
||||
val sessionTopic: String,
|
||||
val status: SessionStatus,
|
||||
) {
|
||||
|
||||
enum class SessionStatus {
|
||||
|
||||
ACTIVE, EXPIRED
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.model
|
||||
|
||||
class WalletConnectSessionProposal(
|
||||
val resolvedChains: SessionChains,
|
||||
val dappMetadata: SessionDappMetadata
|
||||
)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk
|
||||
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.Namespace.Session
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.SessionProposal
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Params.SessionApprove
|
||||
import com.walletconnect.web3.wallet.client.Web3Wallet
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
fun SessionProposal.approved(namespaces: Map<String, Session>): SessionApprove {
|
||||
return SessionApprove(
|
||||
proposerPublicKey = proposerPublicKey,
|
||||
namespaces = namespaces,
|
||||
relayProtocol = relayProtocol
|
||||
)
|
||||
}
|
||||
|
||||
fun SessionProposal.rejected(reason: String): Wallet.Params.SessionReject {
|
||||
return Wallet.Params.SessionReject(
|
||||
proposerPublicKey = proposerPublicKey,
|
||||
reason = reason
|
||||
)
|
||||
}
|
||||
|
||||
fun Wallet.Model.SessionRequest.approved(result: String): Wallet.Params.SessionRequestResponse {
|
||||
return Wallet.Params.SessionRequestResponse(
|
||||
sessionTopic = topic,
|
||||
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
|
||||
id = request.id,
|
||||
result = result
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class WalletConnectError(val code: Int, override val message: String) : Throwable() {
|
||||
|
||||
companion object {
|
||||
val REJECTED = WalletConnectError(5000, "Rejected by user")
|
||||
|
||||
val GENERAL_FAILURE = WalletConnectError(0, "Unknown error")
|
||||
|
||||
val NO_SESSION_FOR_TOPIC = WalletConnectError(7001, "No session for topic")
|
||||
|
||||
val UNAUTHORIZED_METHOD = WalletConnectError(3001, "Unauthorized method")
|
||||
|
||||
val CHAIN_MISMATCH = WalletConnectError(1001, "Wrong chain id passed by dApp")
|
||||
|
||||
fun UnknownMethod(method: String) = WalletConnectError(3001, "$method is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
fun Wallet.Model.SessionRequest.failed(error: WalletConnectError): Wallet.Params.SessionRequestResponse {
|
||||
return Wallet.Params.SessionRequestResponse(
|
||||
sessionTopic = topic,
|
||||
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
|
||||
id = request.id,
|
||||
code = error.code,
|
||||
message = error.message
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun Wallet.Model.SessionRequest.rejected(): Wallet.Params.SessionRequestResponse {
|
||||
return failed(WalletConnectError.REJECTED)
|
||||
}
|
||||
|
||||
suspend fun Web3Wallet.approveSession(approve: SessionApprove): Result<Unit> {
|
||||
return suspendCoroutine { continuation ->
|
||||
approveSession(
|
||||
params = approve,
|
||||
onSuccess = { continuation.resume(Result.success(Unit)) },
|
||||
onError = { continuation.resume(Result.failure(it.throwable)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Web3Wallet.rejectSession(reject: Wallet.Params.SessionReject): Result<Unit> {
|
||||
return suspendCoroutine { continuation ->
|
||||
rejectSession(
|
||||
params = reject,
|
||||
onSuccess = { continuation.resume(Result.success(Unit)) },
|
||||
onError = { continuation.resume(Result.failure(it.throwable)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Web3Wallet.disconnectSession(sessionTopic: String): Result<Unit> {
|
||||
return suspendCoroutine { continuation ->
|
||||
disconnectSession(
|
||||
params = Wallet.Params.SessionDisconnect(sessionTopic),
|
||||
onSuccess = { continuation.resume(Result.success(Unit)) },
|
||||
onError = { continuation.resume(Result.failure(it.throwable)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Web3Wallet.respondSessionRequest(response: Wallet.Params.SessionRequestResponse): Result<Unit> {
|
||||
return suspendCoroutine { continuation ->
|
||||
respondSessionRequest(
|
||||
params = response,
|
||||
onSuccess = { continuation.resume(Result.success(Unit)) },
|
||||
onError = { continuation.resume(Result.failure(it.throwable)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session
|
||||
|
||||
import com.walletconnect.android.Core
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.Namespace
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.SessionProposal
|
||||
import com.walletconnect.web3.wallet.client.Web3Wallet
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Parser
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Resolver
|
||||
import io.novafoundation.nova.caip.caip2.isValidCaip2
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.mapValuesNotNull
|
||||
import io.novafoundation.nova.common.utils.toImmutable
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.WalletConnectPairingRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.WalletConnectSessionRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.SessionChains
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.SessionDappMetadata
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectPairingAccount
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSession
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionDetails
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionDetails.SessionStatus
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionProposal
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.WalletConnectError
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approveSession
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approved
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.disconnectSession
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.rejectSession
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.rejected
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.WalletConnectRequest
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private typealias KnownChainsBuilder = MutableSet<Chain>
|
||||
private typealias UnknownChainsBuilder = MutableSet<String>
|
||||
private typealias Caip2ChainId = String
|
||||
|
||||
class RealWalletConnectSessionInteractor(
|
||||
private val caip2Resolver: Caip2Resolver,
|
||||
private val caip2Parser: Caip2Parser,
|
||||
private val walletConnectRequestFactory: WalletConnectRequest.Factory,
|
||||
private val walletConnectPairingRepository: WalletConnectPairingRepository,
|
||||
private val walletConnectSessionRepository: WalletConnectSessionRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
) : WalletConnectSessionInteractor {
|
||||
|
||||
private val pendingSessionSettlementsByPairingTopic = ConcurrentHashMap<String, PendingSessionSettlement>()
|
||||
|
||||
override suspend fun approveSession(
|
||||
sessionProposal: SessionProposal,
|
||||
metaAccount: MetaAccount
|
||||
): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
val requestedNameSpaces = sessionProposal.requiredNamespaces mergeWith sessionProposal.optionalNamespaces
|
||||
|
||||
val chainsByCaip2 = caip2Resolver.chainsByCaip2()
|
||||
|
||||
val namespaceSessions = requestedNameSpaces.mapValuesNotNull { (namespaceRaw, namespaceProposal) ->
|
||||
val requestedChains = if (caip2Parser.isValidCaip2(namespaceRaw)) {
|
||||
listOf(namespaceRaw)
|
||||
} else {
|
||||
namespaceProposal.chains
|
||||
} ?: return@mapValuesNotNull null
|
||||
|
||||
val supportedChainsWithAccounts = requestedChains.mapNotNull { requestedChain ->
|
||||
val chain = chainsByCaip2[requestedChain] ?: return@mapNotNull null
|
||||
val address = metaAccount.addressIn(chain) ?: return@mapNotNull null
|
||||
|
||||
formatWalletConnectAccount(address, requestedChain) to requestedChain
|
||||
}
|
||||
|
||||
Namespace.Session(
|
||||
chains = supportedChainsWithAccounts.map { (_, chain) -> chain },
|
||||
accounts = supportedChainsWithAccounts.map { (address, _) -> address },
|
||||
methods = namespaceProposal.methods,
|
||||
events = namespaceProposal.events
|
||||
)
|
||||
}
|
||||
|
||||
val response = sessionProposal.approved(namespaceSessions)
|
||||
|
||||
Web3Wallet.approveSession(response)
|
||||
.onSuccess { registerPendingSettlement(sessionProposal, metaAccount) }
|
||||
}
|
||||
|
||||
override suspend fun resolveSessionProposal(sessionProposal: SessionProposal): WalletConnectSessionProposal = withContext(Dispatchers.Default) {
|
||||
val chainsByCaip2 = caip2Resolver.chainsByCaip2()
|
||||
|
||||
WalletConnectSessionProposal(
|
||||
resolvedChains = SessionChains(
|
||||
required = chainsByCaip2.resolveChains(sessionProposal.requiredNamespaces.caip2ChainsByNamespace()),
|
||||
optional = chainsByCaip2.resolveChains(sessionProposal.optionalNamespaces.caip2ChainsByNamespace())
|
||||
),
|
||||
dappMetadata = SessionDappMetadata(
|
||||
dAppUrl = sessionProposal.url,
|
||||
icon = sessionProposal.icons.firstOrNull()?.toString(),
|
||||
name = sessionProposal.name
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun rejectSession(proposal: SessionProposal): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
val response = proposal.rejected("Rejected by user")
|
||||
|
||||
Web3Wallet.rejectSession(response)
|
||||
}
|
||||
|
||||
override suspend fun parseSessionRequest(request: Wallet.Model.SessionRequest): Result<WalletConnectRequest> = runCatching {
|
||||
withContext(Dispatchers.Default) {
|
||||
walletConnectRequestFactory.create(request) ?: throw WalletConnectError.UnknownMethod(request.request.method)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSessionSettled(settledSessionResponse: Wallet.Model.SettledSessionResponse) {
|
||||
if (settledSessionResponse !is Wallet.Model.SettledSessionResponse.Result) return
|
||||
|
||||
val pairingTopic = settledSessionResponse.session.pairingTopic
|
||||
val pendingSessionSettlement = pendingSessionSettlementsByPairingTopic[pairingTopic] ?: return
|
||||
|
||||
val walletConnectPairingAccount = WalletConnectPairingAccount(metaId = pendingSessionSettlement.metaId, pairingTopic = pairingTopic)
|
||||
walletConnectPairingRepository.addPairingAccount(walletConnectPairingAccount)
|
||||
walletConnectSessionRepository.addSession(settledSessionResponse.session)
|
||||
}
|
||||
|
||||
override suspend fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
|
||||
if (sessionDelete !is Wallet.Model.SessionDelete.Success) return
|
||||
|
||||
walletConnectSessionRepository.removeSession(sessionDelete.topic)
|
||||
}
|
||||
|
||||
override suspend fun getSession(sessionTopic: String): Wallet.Model.Session? {
|
||||
return walletConnectSessionRepository.getSession(sessionTopic)
|
||||
}
|
||||
|
||||
override suspend fun getPairingAccount(pairingTopic: String): WalletConnectPairingAccount? {
|
||||
return walletConnectPairingRepository.getPairingAccount(pairingTopic)
|
||||
}
|
||||
|
||||
override fun activeSessionsFlow(metaId: Long?): Flow<List<WalletConnectSession>> {
|
||||
return combine(
|
||||
walletConnectSessionRepository.allSessionsFlow(),
|
||||
walletConnectPairingRepository.allPairingAccountsFlow()
|
||||
) { activeSessions, pairingAccounts ->
|
||||
val metaAccounts = if (metaId == null) {
|
||||
accountRepository.getActiveMetaAccounts()
|
||||
} else {
|
||||
listOf(accountRepository.getMetaAccount(metaId))
|
||||
}
|
||||
|
||||
createWalletSessions(activeSessions, metaAccounts, pairingAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
override fun activeSessionFlow(sessionTopic: String): Flow<WalletConnectSessionDetails?> {
|
||||
val sessionFlow = walletConnectSessionRepository.sessionFlow(sessionTopic)
|
||||
val chainsWrappedFlow = flowOf { caip2Resolver.chainsByCaip2() }
|
||||
|
||||
return combine(sessionFlow, chainsWrappedFlow) { session, chainsByCaip2 ->
|
||||
if (session == null) return@combine null
|
||||
|
||||
val pairingAccount = walletConnectPairingRepository.getPairingAccount(session.pairingTopic) ?: return@combine null
|
||||
val metaAccount = accountRepository.getMetaAccount(pairingAccount.metaId)
|
||||
|
||||
createWalletSessionDetails(session, metaAccount, chainsByCaip2)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun disconnect(sessionTopic: String): Result<*> {
|
||||
return withContext(Dispatchers.Default) {
|
||||
Web3Wallet.disconnectSession(sessionTopic).onSuccess {
|
||||
walletConnectSessionRepository.removeSession(sessionTopic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private infix fun Map<String, Namespace.Proposal>.mergeWith(other: Map<String, Namespace.Proposal>): Map<String, Namespace.Proposal> {
|
||||
val allNamespaceKeys = keys + other.keys
|
||||
|
||||
return allNamespaceKeys.associateWith { namespace ->
|
||||
val thisProposal = get(namespace)
|
||||
val otherProposal = other[namespace]
|
||||
|
||||
thisProposal.orEmpty() + otherProposal.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Namespace.Proposal.plus(other: Namespace.Proposal): Namespace.Proposal {
|
||||
val mergedChains = chains.orEmpty() + other.chains.orEmpty()
|
||||
val mergedMethods = methods + other.methods
|
||||
val mergedEvents = events + other.events
|
||||
return Namespace.Proposal(
|
||||
chains = mergedChains.distinct(),
|
||||
methods = mergedMethods.distinct(),
|
||||
events = mergedEvents.distinct()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Namespace.Proposal?.orEmpty(): Namespace.Proposal {
|
||||
return this ?: Namespace.Proposal(
|
||||
chains = null,
|
||||
methods = emptyList(),
|
||||
events = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatWalletConnectAccount(address: String, chainCaip2: String): String {
|
||||
return "$chainCaip2:$address"
|
||||
}
|
||||
|
||||
private fun registerPendingSettlement(sessionProposal: SessionProposal, metaAccount: MetaAccount) {
|
||||
pendingSessionSettlementsByPairingTopic[sessionProposal.pairingTopic] = PendingSessionSettlement(metaAccount.id)
|
||||
}
|
||||
|
||||
private class PendingSessionSettlement(val metaId: Long)
|
||||
|
||||
private fun createWalletSessions(
|
||||
sessions: List<Wallet.Model.Session>,
|
||||
metaAccounts: List<MetaAccount>,
|
||||
pairingAccounts: List<WalletConnectPairingAccount>
|
||||
): List<WalletConnectSession> {
|
||||
val metaAccountsById = metaAccounts.associateBy(MetaAccount::id)
|
||||
val pairingAccountByPairingTopic = pairingAccounts.associateBy(WalletConnectPairingAccount::pairingTopic)
|
||||
|
||||
return sessions.mapNotNull { session ->
|
||||
val pairingAccount = pairingAccountByPairingTopic[session.pairingTopic] ?: return@mapNotNull null
|
||||
val metaAccount = metaAccountsById[pairingAccount.metaId] ?: return@mapNotNull null
|
||||
|
||||
WalletConnectSession(
|
||||
connectedMetaAccount = metaAccount,
|
||||
dappMetadata = session.metaData?.let(::mapAppMetadataToSessionMetadata),
|
||||
sessionTopic = session.topic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWalletSessionDetails(
|
||||
session: Wallet.Model.Session,
|
||||
metaAccount: MetaAccount,
|
||||
chainsByCaip2: Map<String, Chain>,
|
||||
): WalletConnectSessionDetails {
|
||||
return WalletConnectSessionDetails(
|
||||
connectedMetaAccount = metaAccount,
|
||||
dappMetadata = session.metaData?.let(::mapAppMetadataToSessionMetadata),
|
||||
sessionTopic = session.topic,
|
||||
chains = chainsByCaip2.resolveChains(session.namespaces.caip2ChainsByNamespace()).knownChains,
|
||||
status = determineSessionStatus(session)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Map<String, Chain>.resolveChains(namespaces: Map<String, List<Caip2ChainId>?>): SessionChains.ResolvedChains {
|
||||
val knownChainsBuilder = mutableSetOf<Chain>()
|
||||
val unknownChainsBuilder = mutableSetOf<Caip2ChainId>()
|
||||
|
||||
namespaces.forEach { (namespaceName, namespaceChains) ->
|
||||
if (caip2Parser.isValidCaip2(namespaceName)) {
|
||||
resolveChain(namespaceName, knownChainsBuilder, unknownChainsBuilder)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
namespaceChains.orEmpty().forEach { chainCaip2 ->
|
||||
resolveChain(chainCaip2, knownChainsBuilder, unknownChainsBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
return SessionChains.ResolvedChains(knownChainsBuilder.toImmutable(), unknownChainsBuilder.toImmutable())
|
||||
}
|
||||
|
||||
private fun Map<String, Chain>.resolveChain(
|
||||
chainCaip2: String,
|
||||
knownChainsBuilder: KnownChainsBuilder,
|
||||
unknownChainsBuilder: UnknownChainsBuilder
|
||||
) {
|
||||
val newChain = get(chainCaip2)
|
||||
|
||||
if (newChain != null) {
|
||||
knownChainsBuilder.add(newChain)
|
||||
} else {
|
||||
unknownChainsBuilder.add(chainCaip2)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapAppMetadataToSessionMetadata(metadata: Core.Model.AppMetaData): SessionDappMetadata {
|
||||
return SessionDappMetadata(
|
||||
dAppUrl = metadata.url,
|
||||
icon = metadata.icons.firstOrNull(),
|
||||
name = metadata.name
|
||||
)
|
||||
}
|
||||
|
||||
private fun determineSessionStatus(session: Wallet.Model.Session): SessionStatus {
|
||||
return if (session.expiry > System.currentTimeMillis()) {
|
||||
SessionStatus.EXPIRED
|
||||
} else {
|
||||
SessionStatus.ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("caip2ChainsByNamespaceForProposal")
|
||||
private fun Map<String, Namespace.Proposal>.caip2ChainsByNamespace(): Map<String, List<Caip2ChainId>?> {
|
||||
return mapValues { it.value.chains }
|
||||
}
|
||||
|
||||
@JvmName("caip2ChainsByNamespaceForSession")
|
||||
private fun Map<String, Namespace.Session>.caip2ChainsByNamespace(): Map<String, List<Caip2ChainId>?> {
|
||||
return mapValues { it.value.chains }
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session
|
||||
|
||||
import com.walletconnect.android.CoreClient
|
||||
import com.walletconnect.web3.wallet.client.Web3Wallet
|
||||
import io.novafoundation.nova.common.utils.mapToSet
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.WalletConnectPairingRepository
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.data.repository.WalletConnectSessionRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class RealWalletConnectSessionsUseCase(
|
||||
private val pairingRepository: WalletConnectPairingRepository,
|
||||
private val sessionRepository: WalletConnectSessionRepository,
|
||||
) : WalletConnectSessionsUseCase {
|
||||
|
||||
override fun activeSessionsNumberFlow(): Flow<Int> {
|
||||
return sessionRepository.numberOfSessionsFlow()
|
||||
}
|
||||
|
||||
override fun activeSessionsNumberFlow(metaAccount: MetaAccount): Flow<Int> {
|
||||
return pairingRepository.pairingAccountsByMetaIdFlow(metaAccount.id).flatMapLatest { pairings ->
|
||||
val pairingTopics = pairings.mapToSet { it.pairingTopic }
|
||||
sessionRepository.numberOfSessionsFlow(pairingTopics)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun activeSessionsNumber(): Int {
|
||||
return sessionRepository.numberOfSessionAccounts()
|
||||
}
|
||||
|
||||
override suspend fun syncActiveSessions() = withContext(Dispatchers.Default) {
|
||||
val activePairingTopics = CoreClient.Pairing.getPairings()
|
||||
.filter { it.isActive }
|
||||
.map { it.topic }
|
||||
|
||||
pairingRepository.removeAllPairingsOtherThan(activePairingTopics)
|
||||
|
||||
val activeSessionTopics = Web3Wallet.getListOfActiveSessions()
|
||||
sessionRepository.setSessions(activeSessionTopics)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session
|
||||
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.SessionProposal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectPairingAccount
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSession
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionDetails
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionProposal
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.WalletConnectRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletConnectSessionInteractor {
|
||||
|
||||
suspend fun approveSession(
|
||||
sessionProposal: SessionProposal,
|
||||
metaAccount: MetaAccount
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun resolveSessionProposal(sessionProposal: SessionProposal): WalletConnectSessionProposal
|
||||
|
||||
suspend fun rejectSession(proposal: SessionProposal): Result<Unit>
|
||||
|
||||
suspend fun parseSessionRequest(request: Wallet.Model.SessionRequest): Result<WalletConnectRequest>
|
||||
|
||||
suspend fun onSessionSettled(settledSessionResponse: Wallet.Model.SettledSessionResponse)
|
||||
|
||||
suspend fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete)
|
||||
|
||||
suspend fun getSession(sessionTopic: String): Wallet.Model.Session?
|
||||
|
||||
suspend fun getPairingAccount(pairingTopic: String): WalletConnectPairingAccount?
|
||||
|
||||
fun activeSessionsFlow(metaId: Long?): Flow<List<WalletConnectSession>>
|
||||
|
||||
fun activeSessionFlow(sessionTopic: String): Flow<WalletConnectSessionDetails?>
|
||||
|
||||
suspend fun disconnect(sessionTopic: String): Result<*>
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests
|
||||
|
||||
import android.content.Context
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Web3Wallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.WalletConnectError
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.failed
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.rejected
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.respondSessionRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
abstract class BaseWalletConnectRequest(
|
||||
private val sessionRequest: Wallet.Model.SessionRequest,
|
||||
private val context: Context,
|
||||
) : WalletConnectRequest {
|
||||
|
||||
override val id: String = sessionRequest.request.id.toString()
|
||||
|
||||
abstract suspend fun sentResponse(response: ExternalSignCommunicator.Response.Sent): Wallet.Params.SessionRequestResponse
|
||||
|
||||
abstract suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse
|
||||
|
||||
override suspend fun respondWith(response: ExternalSignCommunicator.Response): Result<*> = kotlin.runCatching {
|
||||
withContext(Dispatchers.Default) {
|
||||
val walletConnectResponse = when (response) {
|
||||
is ExternalSignCommunicator.Response.Rejected -> sessionRequest.rejected()
|
||||
is ExternalSignCommunicator.Response.Sent -> sentResponse(response)
|
||||
is ExternalSignCommunicator.Response.Signed -> signedResponse(response)
|
||||
is ExternalSignCommunicator.Response.SigningFailed -> sessionRequest.failed(WalletConnectError.GENERAL_FAILURE)
|
||||
}
|
||||
|
||||
Web3Wallet.respondSessionRequest(walletConnectResponse).getOrThrow()
|
||||
|
||||
// TODO this code is untested since no dapp currently use redirect param
|
||||
// We cant really enable this code without testing since we need to verify a corner-case when wc is used with redirect param inside dapp browser
|
||||
// This might potentially break user flow since it might direct user to external browser instead of staying in our dapp browser
|
||||
|
||||
// val redirect = sessionRequest.peerMetaData?.redirect
|
||||
// if (!redirect.isNullOrEmpty()) {
|
||||
// context.startActivity(Intent(Intent.ACTION_VIEW, redirect.toUri()))
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SignWalletConnectRequest(
|
||||
sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : BaseWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun sentResponse(response: ExternalSignCommunicator.Response.Sent): Wallet.Params.SessionRequestResponse {
|
||||
error("Expected Signed response, got: Sent")
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SendTxWalletConnectRequest(
|
||||
sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : BaseWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse {
|
||||
error("Expected Sent response, got: Signed")
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests
|
||||
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import io.novasama.substrate_sdk_android.extensions.tryFindNonNull
|
||||
|
||||
class CompoundWalletConnectRequestFactory(
|
||||
private val nestedFactories: List<WalletConnectRequest.Factory>
|
||||
) : WalletConnectRequest.Factory {
|
||||
|
||||
override fun create(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest? {
|
||||
return nestedFactories.tryFindNonNull { it.create(sessionRequest) }
|
||||
}
|
||||
}
|
||||
|
||||
fun CompoundWalletConnectRequestFactory(vararg factories: WalletConnectRequest.Factory): CompoundWalletConnectRequestFactory {
|
||||
return CompoundWalletConnectRequestFactory(factories.toList())
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests
|
||||
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
|
||||
interface WalletConnectRequest {
|
||||
|
||||
interface Factory {
|
||||
|
||||
fun create(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest?
|
||||
}
|
||||
|
||||
val id: String
|
||||
|
||||
suspend fun respondWith(response: ExternalSignCommunicator.Response): Result<*>
|
||||
|
||||
fun toExternalSignRequest(): ExternalSignRequest
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm
|
||||
|
||||
import android.content.Context
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmPersonalSignMessage
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approved
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.SignWalletConnectRequest
|
||||
|
||||
class EvmPersonalSignRequest(
|
||||
private val originAddress: String,
|
||||
private val message: EvmPersonalSignMessage,
|
||||
private val sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : SignWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse {
|
||||
return sessionRequest.approved(response.signature)
|
||||
}
|
||||
|
||||
override fun toExternalSignRequest(): ExternalSignRequest {
|
||||
val signPayload = EvmSignPayload.PersonalSign(message, originAddress)
|
||||
|
||||
return ExternalSignRequest.Evm(id, signPayload)
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm
|
||||
|
||||
import android.content.Context
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Params.SessionRequestResponse
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTransaction
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approved
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.SendTxWalletConnectRequest
|
||||
|
||||
class EvmSendTransactionRequest(
|
||||
private val transaction: EvmTransaction.Struct,
|
||||
private val chainId: Int,
|
||||
private val sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : SendTxWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun sentResponse(response: Response.Sent): SessionRequestResponse {
|
||||
return sessionRequest.approved(response.txHash)
|
||||
}
|
||||
|
||||
override fun toExternalSignRequest(): ExternalSignRequest {
|
||||
val signPayload = EvmSignPayload.ConfirmTx(
|
||||
transaction = transaction,
|
||||
originAddress = transaction.from,
|
||||
chainSource = EvmChainSource(chainId, EvmChainSource.UnknownChainOptions.MustBeKnown),
|
||||
action = EvmSignPayload.ConfirmTx.Action.SEND
|
||||
)
|
||||
|
||||
return ExternalSignRequest.Evm(id, signPayload)
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm
|
||||
|
||||
import android.content.Context
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Params.SessionRequestResponse
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTransaction
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approved
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.SignWalletConnectRequest
|
||||
|
||||
class EvmSignTransactionRequest(
|
||||
private val transaction: EvmTransaction.Struct,
|
||||
private val chainId: Int,
|
||||
private val sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : SignWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun signedResponse(response: Response.Signed): SessionRequestResponse {
|
||||
return sessionRequest.approved(response.signature)
|
||||
}
|
||||
|
||||
override fun toExternalSignRequest(): ExternalSignRequest {
|
||||
val signPayload = EvmSignPayload.ConfirmTx(
|
||||
transaction = transaction,
|
||||
originAddress = transaction.from,
|
||||
chainSource = EvmChainSource(chainId, EvmChainSource.UnknownChainOptions.MustBeKnown),
|
||||
action = EvmSignPayload.ConfirmTx.Action.SIGN
|
||||
)
|
||||
|
||||
return ExternalSignRequest.Evm(id, signPayload)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm
|
||||
|
||||
import android.content.Context
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Params.SessionRequestResponse
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTypedMessage
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approved
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.SignWalletConnectRequest
|
||||
|
||||
class EvmSignTypedDataRequest(
|
||||
private val originAddress: String,
|
||||
private val typedMessage: EvmTypedMessage,
|
||||
private val sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : SignWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun signedResponse(response: Response.Signed): SessionRequestResponse {
|
||||
return sessionRequest.approved(response.signature)
|
||||
}
|
||||
|
||||
override fun toExternalSignRequest(): ExternalSignRequest {
|
||||
val signPayload = EvmSignPayload.SignTypedMessage(typedMessage, originAddress)
|
||||
|
||||
return ExternalSignRequest.Evm(id, signPayload)
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Parser
|
||||
import io.novafoundation.nova.caip.caip2.identifier.Caip2Identifier
|
||||
import io.novafoundation.nova.common.utils.castOrNull
|
||||
import io.novafoundation.nova.common.utils.fromJson
|
||||
import io.novafoundation.nova.feature_external_sign_api.domain.sign.evm.EvmTypedMessageParser
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmPersonalSignMessage
|
||||
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_wallet_connect_impl.data.model.evm.WalletConnectEvmTransaction
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.WalletConnectRequest
|
||||
|
||||
class EvmWalletConnectRequestFactory(
|
||||
private val gson: Gson,
|
||||
private val caip2Parser: Caip2Parser,
|
||||
private val typedMessageParser: EvmTypedMessageParser,
|
||||
private val context: Context
|
||||
) : WalletConnectRequest.Factory {
|
||||
|
||||
override fun create(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest? {
|
||||
val request = sessionRequest.request
|
||||
|
||||
return when (request.method) {
|
||||
"eth_sendTransaction" -> parseEvmSendTx(sessionRequest, sessionRequest.eipChainId())
|
||||
|
||||
"eth_signTransaction" -> parseEvmSignTx(sessionRequest, sessionRequest.eipChainId())
|
||||
|
||||
"eth_signTypedData", "eth_signTypedData_v4" -> parseEvmSignTypedMessage(sessionRequest)
|
||||
|
||||
"personal_sign" -> parsePersonalSign(sessionRequest)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parsePersonalSign(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest {
|
||||
val (message, address) = gson.fromJson<List<String>>(sessionRequest.request.params)
|
||||
val personalSignMessage = EvmPersonalSignMessage(message)
|
||||
|
||||
return EvmPersonalSignRequest(address, personalSignMessage, sessionRequest, context)
|
||||
}
|
||||
|
||||
private fun parseEvmSignTypedMessage(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest {
|
||||
val (address, typedMessage) = parseEvmSignTypedDataParams(sessionRequest.request.params)
|
||||
|
||||
return EvmSignTypedDataRequest(address, typedMessage, sessionRequest, context)
|
||||
}
|
||||
|
||||
private fun parseEvmSendTx(sessionRequest: Wallet.Model.SessionRequest, chainId: Int): WalletConnectRequest {
|
||||
val transaction = parseStructTransaction(sessionRequest.request.params)
|
||||
|
||||
return EvmSendTransactionRequest(transaction, chainId, sessionRequest, context)
|
||||
}
|
||||
|
||||
private fun parseEvmSignTx(sessionRequest: Wallet.Model.SessionRequest, chainId: Int): WalletConnectRequest {
|
||||
val transaction = parseStructTransaction(sessionRequest.request.params)
|
||||
|
||||
return EvmSignTransactionRequest(transaction, chainId, sessionRequest, context)
|
||||
}
|
||||
|
||||
private fun Wallet.Model.SessionRequest.eipChainId(): Int {
|
||||
return chainId?.let(::extractEvmChainId)
|
||||
?: error("No chain id supplied for ${request.method}")
|
||||
}
|
||||
|
||||
private fun parseStructTransaction(params: String): EvmTransaction.Struct {
|
||||
val parsed: WalletConnectEvmTransaction = parseSingleEvmParameter(params)
|
||||
|
||||
return with(parsed) {
|
||||
EvmTransaction.Struct(
|
||||
gas = gasLimit,
|
||||
gasPrice = gasPrice,
|
||||
from = from,
|
||||
to = to,
|
||||
data = data,
|
||||
value = value,
|
||||
nonce = nonce
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEvmSignTypedDataParams(params: String): Pair<String, EvmTypedMessage> {
|
||||
// params = ["addressParam", structuredDataObject]
|
||||
val (addressParam, structuredData) = params.removeSurrounding("[", "]").split(',', limit = 2)
|
||||
val address = addressParam.removeSurrounding("\"")
|
||||
|
||||
val evmTypedMessage = typedMessageParser.parseEvmTypedMessage(structuredData)
|
||||
|
||||
return address to evmTypedMessage
|
||||
}
|
||||
|
||||
private inline fun <reified T : List<I>, reified I> parseSingleEvmParameter(params: String): I {
|
||||
// gson.fromJson<List<I>>(params) does not work even with inlining - gson ignores inner list types and creates hash map instead
|
||||
val parsed = gson.fromJson<T>(params)
|
||||
|
||||
return parsed.first()
|
||||
}
|
||||
|
||||
private fun extractEvmChainId(caip2: String): Int? {
|
||||
return caip2Parser.parseCaip2(caip2)
|
||||
.getOrNull()
|
||||
?.castOrNull<Caip2Identifier.Eip155>()
|
||||
?.chainId?.toInt()
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.polkadot
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignerResult
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.approved
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.SignWalletConnectRequest
|
||||
|
||||
class PolkadotSignRequest(
|
||||
private val gson: Gson,
|
||||
private val polkadotSignPayload: PolkadotSignPayload,
|
||||
private val sessionRequest: Wallet.Model.SessionRequest,
|
||||
context: Context
|
||||
) : SignWalletConnectRequest(sessionRequest, context) {
|
||||
|
||||
override suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse {
|
||||
val responseData = PolkadotSignerResult(id, signature = response.signature, response.modifiedTransaction)
|
||||
val responseJson = gson.toJson(responseData)
|
||||
|
||||
return sessionRequest.approved(responseJson)
|
||||
}
|
||||
|
||||
override fun toExternalSignRequest(): ExternalSignRequest {
|
||||
return ExternalSignRequest.Polkadot(id, polkadotSignPayload)
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.polkadot
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.SessionRequest
|
||||
import io.novafoundation.nova.caip.caip2.Caip2Parser
|
||||
import io.novafoundation.nova.caip.caip2.identifier.Caip2Identifier
|
||||
import io.novafoundation.nova.caip.caip2.parseCaip2OrThrow
|
||||
import io.novafoundation.nova.common.utils.fromJson
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignPayload
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.WalletConnectError
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.WalletConnectRequest
|
||||
|
||||
class PolkadotWalletConnectRequestFactory(
|
||||
private val gson: Gson,
|
||||
private val caip2Parser: Caip2Parser,
|
||||
private val context: Context
|
||||
) : WalletConnectRequest.Factory {
|
||||
|
||||
override fun create(sessionRequest: SessionRequest): WalletConnectRequest? {
|
||||
val request = sessionRequest.request
|
||||
|
||||
return when (request.method) {
|
||||
"polkadot_signTransaction" -> parseSignTransactionRequest(sessionRequest)
|
||||
|
||||
"polkadot_signMessage" -> parseSignMessageRequest(sessionRequest)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSignTransactionRequest(sessionRequest: SessionRequest): WalletConnectRequest {
|
||||
val signTxPayload = gson.fromJson<SignTransaction>(sessionRequest.request.params)
|
||||
|
||||
val caip2FromPayload = Caip2Identifier.Polkadot(signTxPayload.transactionPayload.genesisHash)
|
||||
val caip2FromChainId = caip2Parser.parseCaip2OrThrow(requireNotNull(sessionRequest.chainId))
|
||||
|
||||
if (caip2FromChainId != caip2FromPayload) throw WalletConnectError.CHAIN_MISMATCH
|
||||
|
||||
return PolkadotSignRequest(
|
||||
gson = gson,
|
||||
polkadotSignPayload = signTxPayload.transactionPayload,
|
||||
sessionRequest = sessionRequest,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSignMessageRequest(sessionRequest: SessionRequest): WalletConnectRequest {
|
||||
val signMessagePayload = gson.fromJson<SignMessage>(sessionRequest.request.params)
|
||||
|
||||
return PolkadotSignRequest(
|
||||
gson = gson,
|
||||
polkadotSignPayload = PolkadotSignPayload.Raw(
|
||||
data = signMessagePayload.message,
|
||||
address = signMessagePayload.address,
|
||||
type = null
|
||||
),
|
||||
sessionRequest = sessionRequest,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SignTransaction(val transactionPayload: PolkadotSignPayload.Json)
|
||||
|
||||
private class SignMessage(val address: String, val message: String)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.utils.WalletConnectUtils
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
class WalletConnectPairDeeplinkHandler(
|
||||
private val walletConnectService: WalletConnectService,
|
||||
private val automaticInteractionGate: AutomaticInteractionGate
|
||||
) : DeepLinkHandler {
|
||||
|
||||
override val callbackFlow: Flow<CallbackEvent> = emptyFlow()
|
||||
|
||||
override suspend fun matches(data: Uri): Boolean {
|
||||
return WalletConnectUtils.isWalletConnectPairingLink(data)
|
||||
}
|
||||
|
||||
override suspend fun handleDeepLink(data: Uri) = runCatching {
|
||||
automaticInteractionGate.awaitInteractionAllowed()
|
||||
walletConnectService.pair(data.toString())
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.scan
|
||||
|
||||
import android.view.View
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.presentation.scan.ScanQrFragment
|
||||
import io.novafoundation.nova.common.presentation.scan.ScanView
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.setDrawableStart
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.databinding.FragmentWcScanBinding
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.di.WalletConnectFeatureComponent
|
||||
|
||||
class WalletConnectScanFragment : ScanQrFragment<WalletConnectScanViewModel, FragmentWcScanBinding>() {
|
||||
|
||||
override fun createBinding() = FragmentWcScanBinding.inflate(layoutInflater)
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<WalletConnectFeatureComponent>(requireContext(), WalletConnectFeatureApi::class.java)
|
||||
.walletConnectScanComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.walletConnectScanToolbar.applyStatusBarInsets()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
super.initViews()
|
||||
|
||||
binder.walletConnectScanToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
scanView.subtitle.setDrawableStart(R.drawable.ic_wallet_connect, widthInDp = 24, paddingInDp = 2, tint = R.color.icon_primary)
|
||||
scanView.subtitle.setTextAppearance(R.style.TextAppearance_NovaFoundation_SemiBold_SubHeadline)
|
||||
scanView.subtitle.setTextColorRes(R.color.text_primary)
|
||||
}
|
||||
|
||||
override val scanView: ScanView
|
||||
get() = binder.walletConnectScan
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.scan
|
||||
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
import io.novafoundation.nova.common.presentation.scan.ScanQrViewModel
|
||||
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
|
||||
class WalletConnectScanViewModel(
|
||||
private val router: ReturnableRouter,
|
||||
private val permissionsAsker: PermissionsAsker.Presentation,
|
||||
private val walletConnectService: WalletConnectService
|
||||
) : ScanQrViewModel(permissionsAsker) {
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
override suspend fun scanned(result: String) {
|
||||
initiatePairing(result)
|
||||
|
||||
router.back()
|
||||
}
|
||||
|
||||
private fun initiatePairing(uri: String) {
|
||||
walletConnectService.pair(uri)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.scan.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_wallet_connect_impl.presentation.scan.WalletConnectScanFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
WalletConnectScanModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface WalletConnectScanComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): WalletConnectScanComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: WalletConnectScanFragment)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.scan.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.common.utils.permissions.PermissionsAsker
|
||||
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.scan.WalletConnectScanViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class WalletConnectScanModule {
|
||||
|
||||
@Provides
|
||||
fun providePermissionAsker(
|
||||
permissionsAskerFactory: PermissionsAskerFactory,
|
||||
fragment: Fragment,
|
||||
router: WalletConnectRouter
|
||||
) = permissionsAskerFactory.createReturnable(fragment, router)
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(WalletConnectScanViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: WalletConnectRouter,
|
||||
permissionsAsker: PermissionsAsker.Presentation,
|
||||
walletConnectService: WalletConnectService
|
||||
): ViewModel {
|
||||
return WalletConnectScanViewModel(router, permissionsAsker, walletConnectService)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): WalletConnectScanViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(WalletConnectScanViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.service
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.walletconnect.android.Core
|
||||
import com.walletconnect.android.CoreClient
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Web3Wallet
|
||||
import io.novafoundation.nova.common.navigation.awaitResponse
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignRequester
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.awaitConfirmation
|
||||
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_api.model.signPayload.SigningDappMetadata
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.WalletConnectError
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.failed
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.sdk.respondSessionRequest
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.ApproveSessionRequester
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.WalletConnectSessionsEvent
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.sessionEventsFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class RealWalletConnectService(
|
||||
parentScope: CoroutineScope,
|
||||
private val interactor: WalletConnectSessionInteractor,
|
||||
private val dAppSignRequester: ExternalSignRequester,
|
||||
private val approveSessionRequester: ApproveSessionRequester,
|
||||
) : WalletConnectService,
|
||||
CoroutineScope by parentScope,
|
||||
WithCoroutineScopeExtensions by WithCoroutineScopeExtensions(parentScope) {
|
||||
|
||||
private val events = Web3Wallet.sessionEventsFlow(scope = this)
|
||||
|
||||
override val onPairErrorLiveData: MutableLiveData<Event<Throwable>> = MutableLiveData()
|
||||
|
||||
init {
|
||||
events.onEach {
|
||||
when (it) {
|
||||
is WalletConnectSessionsEvent.SessionProposal -> handleSessionProposal(it.proposal)
|
||||
is WalletConnectSessionsEvent.SessionRequest -> handleSessionRequest(it.request)
|
||||
is WalletConnectSessionsEvent.SessionSettlement -> handleSessionSettlement(it.settlement)
|
||||
is WalletConnectSessionsEvent.SessionDeleted -> handleSessionDelete(it.delete)
|
||||
}
|
||||
}
|
||||
.inBackground()
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
CoreClient.Relay.connect { error: Core.Model.Error ->
|
||||
Log.d(LOG_TAG, "Failed to connect to Wallet Connect: ", error.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
CoreClient.Relay.disconnect { error: Core.Model.Error ->
|
||||
Log.d(LOG_TAG, "Failed to disconnect to Wallet Connect: ", error.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pair(uri: String) {
|
||||
Web3Wallet.pair(Wallet.Params.Pair(uri), onError = { onPairErrorLiveData.postValue(Event(it.throwable)) })
|
||||
}
|
||||
|
||||
private suspend fun handleSessionProposal(proposal: Wallet.Model.SessionProposal) = withContext(Dispatchers.Main) {
|
||||
approveSessionRequester.awaitResponse(proposal)
|
||||
}
|
||||
|
||||
private suspend fun handleSessionRequest(sessionRequest: Wallet.Model.SessionRequest) {
|
||||
val sdkSession = interactor.getSession(sessionRequest.topic) ?: run { respondNoSession(sessionRequest); return }
|
||||
val appPairing = interactor.getPairingAccount(sdkSession.pairingTopic) ?: run { respondNoSession(sessionRequest); return }
|
||||
|
||||
val walletConnectRequest = interactor.parseSessionRequest(sessionRequest)
|
||||
.onFailure { error ->
|
||||
Log.e("WalletConnect", "Failed to parse session request $sessionRequest", error)
|
||||
|
||||
respondWithError(sessionRequest, error)
|
||||
|
||||
return
|
||||
}.getOrThrow()
|
||||
|
||||
val externalSignResponse = withContext(Dispatchers.Main) {
|
||||
dAppSignRequester.awaitConfirmation(
|
||||
ExternalSignPayload(
|
||||
signRequest = walletConnectRequest.toExternalSignRequest(),
|
||||
dappMetadata = mapWalletConnectSessionToSignDAppMetadata(sdkSession),
|
||||
wallet = ExternalSignWallet.WithId(appPairing.metaId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
walletConnectRequest.respondWith(externalSignResponse)
|
||||
}
|
||||
|
||||
private suspend fun handleSessionSettlement(settlement: Wallet.Model.SettledSessionResponse) {
|
||||
interactor.onSessionSettled(settlement)
|
||||
}
|
||||
|
||||
private suspend fun handleSessionDelete(settlement: Wallet.Model.SessionDelete) {
|
||||
interactor.onSessionDelete(settlement)
|
||||
}
|
||||
|
||||
private fun mapWalletConnectSessionToSignDAppMetadata(session: Wallet.Model.Session): SigningDappMetadata? {
|
||||
return session.metaData?.run {
|
||||
SigningDappMetadata(
|
||||
icon = icons.firstOrNull(),
|
||||
name = name,
|
||||
url = url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun respondNoSession(
|
||||
sessionRequest: Wallet.Model.SessionRequest,
|
||||
): Result<*> {
|
||||
val response = sessionRequest.failed(WalletConnectError.NO_SESSION_FOR_TOPIC)
|
||||
|
||||
return Web3Wallet.respondSessionRequest(response)
|
||||
}
|
||||
|
||||
private suspend fun respondWithError(
|
||||
sessionRequest: Wallet.Model.SessionRequest,
|
||||
exception: Throwable
|
||||
): Result<*> {
|
||||
val error = exception as? WalletConnectError ?: WalletConnectError.GENERAL_FAILURE
|
||||
val response = sessionRequest.failed(error)
|
||||
|
||||
return Web3Wallet.respondSessionRequest(response)
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve
|
||||
|
||||
import com.walletconnect.web3.wallet.client.Wallet.Model.SessionProposal
|
||||
import io.novafoundation.nova.common.navigation.InterScreenRequester
|
||||
import io.novafoundation.nova.common.navigation.InterScreenResponder
|
||||
|
||||
interface ApproveSessionRequester : InterScreenRequester<SessionProposal, Unit>
|
||||
|
||||
interface ApproveSessionResponder : InterScreenResponder<SessionProposal, Unit>
|
||||
|
||||
interface ApproveSessionCommunicator : ApproveSessionRequester, ApproveSessionResponder
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.common.view.setMessageOrHide
|
||||
import io.novafoundation.nova.common.view.showValueOrHide
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.showChainsOverview
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.setupSelectWalletMixin
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.databinding.FragmentWcSessionApproveBinding
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.di.WalletConnectFeatureComponent
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.view.WCNetworksBottomSheet
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletConnectApproveSessionFragment : BaseFragment<WalletConnectApproveSessionViewModel, FragmentWcSessionApproveBinding>() {
|
||||
|
||||
override fun createBinding() = FragmentWcSessionApproveBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun initViews() {
|
||||
onBackPressed { viewModel.exit() }
|
||||
|
||||
binder.wcApproveSessionToolbar.setHomeButtonListener { viewModel.exit() }
|
||||
|
||||
binder.wcApproveSessionReject.setOnClickListener { viewModel.rejectClicked() }
|
||||
binder.wcApproveSessionReject.prepareForProgress(viewLifecycleOwner)
|
||||
|
||||
binder.wcApproveSessionAllow.prepareForProgress(viewLifecycleOwner)
|
||||
binder.wcApproveSessionAllow.setOnClickListener { viewModel.approveClicked() }
|
||||
|
||||
binder.wcApproveSessionNetworks.setOnClickListener { viewModel.networksClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<WalletConnectFeatureComponent>(
|
||||
requireContext(),
|
||||
WalletConnectFeatureApi::class.java
|
||||
)
|
||||
.walletConnectApproveSessionComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: WalletConnectApproveSessionViewModel) {
|
||||
setupSelectWalletMixin(viewModel.selectWalletMixin, binder.wcApproveSessionWallet)
|
||||
|
||||
viewModel.sessionMetadata.observe { sessionMetadata ->
|
||||
binder.wcApproveSessionDApp.showValueOrHide(sessionMetadata.dAppUrl)
|
||||
binder.wcApproveSessionIcon.showDAppIcon(sessionMetadata.icon, imageLoader)
|
||||
}
|
||||
|
||||
viewModel.chainsOverviewFlow.observe(binder.wcApproveSessionNetworks::showChainsOverview)
|
||||
|
||||
viewModel.title.observe(binder.wcApproveSessionTitle::setText)
|
||||
|
||||
viewModel.sessionAlerts.observe { sessionAlerts ->
|
||||
binder.wcApproveSessionChainsAlert.setMessageOrHide(sessionAlerts.unsupportedChains?.alertContent)
|
||||
binder.wcApproveSessionAccountsAlert.setMessageOrHide(sessionAlerts.missingAccounts?.alertContent)
|
||||
}
|
||||
|
||||
viewModel.allowButtonState.observe(binder.wcApproveSessionAllow::setState)
|
||||
viewModel.rejectButtonState.observe(binder.wcApproveSessionReject::setState)
|
||||
|
||||
viewModel.showNetworksBottomSheet.observeEvent { data ->
|
||||
WCNetworksBottomSheet(context = requireContext(), data = data)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.navigation.requireLastInput
|
||||
import io.novafoundation.nova.common.navigation.respond
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.resources.formatListPreview
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainListOverview
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.iconOrFallback
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletMixin
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.selectedMetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.SessionChains
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionProposal
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.allKnownChains
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.allUnknownChains
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.dAppTitle
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.hasAny
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.hasUnknown
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.model.SessionAlerts
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.model.hasBlockingAlerts
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.view.WCNetworkListModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MISSING_ACCOUNTS_PREVIEW_SIZE = 3
|
||||
|
||||
class WalletConnectApproveSessionViewModel(
|
||||
private val router: WalletConnectRouter,
|
||||
private val interactor: WalletConnectSessionInteractor,
|
||||
private val responder: ApproveSessionResponder,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val selectWalletMixinFactory: SelectWalletMixin.Factory
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val proposal = responder.requireLastInput()
|
||||
|
||||
val selectWalletMixin = selectWalletMixinFactory.create(
|
||||
coroutineScope = this,
|
||||
selectionParams = ::walletSelectionParams
|
||||
)
|
||||
|
||||
private val processState = MutableStateFlow(ProgressState.IDLE)
|
||||
|
||||
private val sessionProposalFlow = flowOf {
|
||||
interactor.resolveSessionProposal(proposal)
|
||||
}.shareInBackground()
|
||||
|
||||
val sessionMetadata = sessionProposalFlow.map { it.dappMetadata }
|
||||
|
||||
val title = sessionMetadata.map { sessionDAppMetadata ->
|
||||
val dAppTitle = sessionDAppMetadata.dAppTitle
|
||||
|
||||
resourceManager.getString(R.string.dapp_confirm_authorize_title_format, dAppTitle)
|
||||
}.shareInBackground()
|
||||
|
||||
val chainsOverviewFlow = sessionProposalFlow.map { sessionProposal ->
|
||||
createSessionNetworksModel(sessionProposal.resolvedChains)
|
||||
}.shareInBackground()
|
||||
|
||||
val sessionAlerts = combine(selectWalletMixin.selectedMetaAccountFlow, sessionProposalFlow) { metaAccount, sessionProposal ->
|
||||
constructSessionAlerts(metaAccount, sessionProposal)
|
||||
}.shareInBackground()
|
||||
|
||||
val networksListFlow = sessionProposalFlow.map { constructNetworksList(it.resolvedChains) }
|
||||
.shareInBackground()
|
||||
|
||||
val allowButtonState = allowButtonState().shareInBackground()
|
||||
val rejectButtonState = rejectButtonState().shareInBackground()
|
||||
|
||||
private val _showNetworksBottomSheet = MutableLiveData<Event<List<WCNetworkListModel>>>()
|
||||
val showNetworksBottomSheet: LiveData<Event<List<WCNetworkListModel>>> = _showNetworksBottomSheet
|
||||
|
||||
fun exit() {
|
||||
rejectClicked()
|
||||
}
|
||||
|
||||
fun rejectClicked() = launch {
|
||||
if (isInProgress()) return@launch
|
||||
processState.value = ProgressState.REJECTING
|
||||
|
||||
val proposal = responder.requireLastInput()
|
||||
|
||||
interactor.rejectSession(proposal)
|
||||
responder.respond()
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun approveClicked() = launch {
|
||||
if (isInProgress()) return@launch
|
||||
processState.value = ProgressState.CONFIRMING
|
||||
|
||||
val proposal = responder.requireLastInput()
|
||||
val metaAccount = selectWalletMixin.selectedMetaAccount()
|
||||
|
||||
interactor.approveSession(proposal, metaAccount)
|
||||
.onFailure {
|
||||
Log.d("WalletConnect", "Session approve failed", it)
|
||||
}
|
||||
|
||||
responder.respond()
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun networksClicked() = launch {
|
||||
_showNetworksBottomSheet.value = networksListFlow.first().event()
|
||||
}
|
||||
|
||||
private suspend fun walletSelectionParams(): SelectWalletMixin.SelectionParams {
|
||||
val pairingAccount = interactor.getPairingAccount(proposal.pairingTopic)
|
||||
|
||||
return if (pairingAccount != null) {
|
||||
SelectWalletMixin.SelectionParams(
|
||||
selectionAllowed = false,
|
||||
initialSelection = SelectWalletMixin.InitialSelection.SpecificWallet(pairingAccount.metaId)
|
||||
)
|
||||
} else {
|
||||
SelectWalletMixin.SelectionParams(
|
||||
selectionAllowed = true,
|
||||
initialSelection = SelectWalletMixin.InitialSelection.ActiveWallet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructSessionAlerts(metaAccount: MetaAccount, sessionProposal: WalletConnectSessionProposal): SessionAlerts {
|
||||
val chains = sessionProposal.resolvedChains
|
||||
|
||||
val unsupportedChainsAlert = if (chains.required.hasUnknown()) {
|
||||
val content = resourceManager.getString(R.string.wallet_connect_session_approve_unsupported_chains_alert, sessionProposal.dappMetadata.dAppTitle)
|
||||
|
||||
SessionAlerts.UnsupportedChains(content)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val chainsWithMissingAccounts = metaAccount.findMissingAccountsFor(chains.required.knownChains)
|
||||
|
||||
val missingAccountsAlert = if (chainsWithMissingAccounts.isNotEmpty()) {
|
||||
val missingChainNames = chainsWithMissingAccounts.map { it.name }
|
||||
val missingChains = resourceManager.formatListPreview(missingChainNames, maxPreviewItems = MISSING_ACCOUNTS_PREVIEW_SIZE)
|
||||
val content = resourceManager.getQuantityString(
|
||||
R.plurals.wallet_connect_session_approve_missing_accounts_alert,
|
||||
chainsWithMissingAccounts.size,
|
||||
missingChains
|
||||
)
|
||||
|
||||
SessionAlerts.MissingAccounts(content)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return SessionAlerts(
|
||||
missingAccounts = missingAccountsAlert,
|
||||
unsupportedChains = unsupportedChainsAlert
|
||||
)
|
||||
}
|
||||
|
||||
private fun rejectButtonState(): Flow<DescriptiveButtonState> {
|
||||
return processState.map { progressState ->
|
||||
when (progressState) {
|
||||
ProgressState.IDLE -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.common_reject))
|
||||
|
||||
ProgressState.REJECTING -> DescriptiveButtonState.Loading
|
||||
|
||||
else -> DescriptiveButtonState.Disabled(resourceManager.getString(R.string.common_reject))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun allowButtonState(): Flow<DescriptiveButtonState> {
|
||||
return combine(processState, sessionAlerts) { progressState, sessionAlerts ->
|
||||
when {
|
||||
sessionAlerts.hasBlockingAlerts() -> DescriptiveButtonState.Gone
|
||||
|
||||
progressState == ProgressState.IDLE -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.common_allow))
|
||||
|
||||
progressState == ProgressState.CONFIRMING -> DescriptiveButtonState.Loading
|
||||
|
||||
else -> DescriptiveButtonState.Disabled(resourceManager.getString(R.string.common_allow))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInProgress(): Boolean {
|
||||
return processState.value != ProgressState.IDLE
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
private fun createSessionNetworksModel(sessionChains: SessionChains): ChainListOverview {
|
||||
val allKnownChains = sessionChains.allKnownChains()
|
||||
val allUnknownChains = sessionChains.allUnknownChains()
|
||||
|
||||
val allChainsCount = allKnownChains.size + allUnknownChains.size
|
||||
|
||||
val value = when {
|
||||
// no chains
|
||||
allKnownChains.isEmpty() && allUnknownChains.isEmpty() -> resourceManager.getString(R.string.common_none)
|
||||
|
||||
// only unknown chains
|
||||
allKnownChains.isEmpty() && allUnknownChains.isNotEmpty() -> {
|
||||
resourceManager.getQuantityString(R.plurals.common_unknown_chains, allUnknownChains.size, allUnknownChains.size)
|
||||
}
|
||||
|
||||
// single known chain
|
||||
allKnownChains.size == 1 && allUnknownChains.isEmpty() -> {
|
||||
allKnownChains.single().name
|
||||
}
|
||||
|
||||
// multiple known and unknown chains
|
||||
else -> {
|
||||
val previewItem = allKnownChains.first().name
|
||||
val othersCount = allChainsCount - 1
|
||||
|
||||
resourceManager.getString(R.string.common_element_and_more_format, previewItem, othersCount)
|
||||
}
|
||||
}
|
||||
|
||||
val multipleChainsRequested = allChainsCount > 1
|
||||
val hasUnsupportedWarningsToShow = allUnknownChains.isNotEmpty()
|
||||
|
||||
val firstKnownIcon = allKnownChains.firstOrNull()?.iconOrFallback()
|
||||
|
||||
return ChainListOverview(
|
||||
icon = firstKnownIcon?.takeUnless { multipleChainsRequested },
|
||||
value = value,
|
||||
label = resourceManager.getQuantityString(R.plurals.common_networks_plural, allChainsCount),
|
||||
hasMoreElements = multipleChainsRequested || hasUnsupportedWarningsToShow
|
||||
)
|
||||
}
|
||||
|
||||
private fun MetaAccount.findMissingAccountsFor(chains: Collection<Chain>): List<Chain> {
|
||||
return chains.filterNot(::hasAccountIn)
|
||||
}
|
||||
|
||||
private fun constructNetworksList(sessionChains: SessionChains): List<WCNetworkListModel> {
|
||||
return buildList {
|
||||
addCategory(sessionChains.required, R.string.common_required)
|
||||
addCategory(sessionChains.optional, R.string.common_optional)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WCNetworkListModel>.addCategory(resolvedChains: SessionChains.ResolvedChains, @StringRes categoryNameRes: Int) {
|
||||
if (resolvedChains.hasAny()) {
|
||||
val element = WCNetworkListModel.Label(name = resourceManager.getString(categoryNameRes), needsAdditionalSeparator = true)
|
||||
add(element)
|
||||
}
|
||||
|
||||
val knownChainsUi = resolvedChains.knownChains.map { WCNetworkListModel.Chain(mapChainToUi(it)) }
|
||||
addAll(knownChainsUi)
|
||||
|
||||
if (resolvedChains.hasUnknown()) {
|
||||
val unknownCount = resolvedChains.unknownChains.size
|
||||
val unknownLabel = resourceManager.getQuantityString(R.plurals.wallet_connect_unsupported_networks_hidden, unknownCount, unknownCount)
|
||||
val element = WCNetworkListModel.Label(name = unknownLabel, needsAdditionalSeparator = false)
|
||||
add(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ProgressState {
|
||||
IDLE, CONFIRMING, REJECTING
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.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_wallet_connect_impl.presentation.sessions.approve.WalletConnectApproveSessionFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
WalletConnectApproveSessionModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface WalletConnectApproveSessionComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): WalletConnectApproveSessionComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: WalletConnectApproveSessionFragment)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.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.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletMixin
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.ApproveSessionCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.WalletConnectApproveSessionViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class WalletConnectApproveSessionModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(WalletConnectApproveSessionViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: WalletConnectRouter,
|
||||
interactor: WalletConnectSessionInteractor,
|
||||
communicator: ApproveSessionCommunicator,
|
||||
resourceManager: ResourceManager,
|
||||
selectWalletMixinFactory: SelectWalletMixin.Factory
|
||||
): ViewModel {
|
||||
return WalletConnectApproveSessionViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
responder = communicator,
|
||||
resourceManager = resourceManager,
|
||||
selectWalletMixinFactory = selectWalletMixinFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): WalletConnectApproveSessionViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(WalletConnectApproveSessionViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.model
|
||||
|
||||
class SessionAlerts(
|
||||
val missingAccounts: MissingAccounts?,
|
||||
val unsupportedChains: UnsupportedChains?
|
||||
) {
|
||||
|
||||
class MissingAccounts(val alertContent: String)
|
||||
|
||||
class UnsupportedChains(val alertContent: String)
|
||||
}
|
||||
|
||||
fun SessionAlerts.hasBlockingAlerts(): Boolean {
|
||||
return missingAccounts != null || unsupportedChains != null
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainListOverview
|
||||
|
||||
class SessionNetworksModel(
|
||||
val label: String,
|
||||
val value: ChainListOverview
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.view
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
|
||||
sealed class WCNetworkListModel {
|
||||
|
||||
class Label(val name: String, val needsAdditionalSeparator: Boolean) : WCNetworkListModel()
|
||||
|
||||
class Chain(val chainUi: ChainUi) : WCNetworkListModel()
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.view
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import io.novafoundation.nova.common.list.BaseGroupedDiffCallback
|
||||
import io.novafoundation.nova.common.list.GroupedListAdapter
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.utils.dp
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_account_api.databinding.ItemBottomSheetChainListBinding
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_account_api.view.ChainChipView
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.databinding.ItemBottomSheetWcNetworksLabelBinding
|
||||
|
||||
class WCNetworksAdapter : GroupedListAdapter<WCNetworkListModel.Label, WCNetworkListModel.Chain>(WCNetworksDiffCallback()) {
|
||||
|
||||
override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return WcNetworksLabelHolder(ItemBottomSheetWcNetworksLabelBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return WcNetworksChainHolder(ItemBottomSheetChainListBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun bindChild(holder: GroupedListHolder, child: WCNetworkListModel.Chain) {
|
||||
(holder as WcNetworksChainHolder).bind(child.chainUi)
|
||||
}
|
||||
|
||||
override fun bindGroup(holder: GroupedListHolder, group: WCNetworkListModel.Label) {
|
||||
(holder as WcNetworksLabelHolder).bind(group)
|
||||
}
|
||||
}
|
||||
|
||||
private class WcNetworksChainHolder(private val binder: ItemBottomSheetChainListBinding) : GroupedListHolder(binder.root) {
|
||||
|
||||
private val chainChipView = containerView as ChainChipView
|
||||
|
||||
fun bind(item: ChainUi) {
|
||||
chainChipView.setChain(item)
|
||||
}
|
||||
}
|
||||
|
||||
private class WcNetworksLabelHolder(private val binder: ItemBottomSheetWcNetworksLabelBinding) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(item: WCNetworkListModel.Label) = with(binder.root) {
|
||||
updateLayoutParams<MarginLayoutParams> {
|
||||
if (item.needsAdditionalSeparator) {
|
||||
setMargins(16.dp(context), 12.dp(context), 16.dp(context), 4.dp(context))
|
||||
} else {
|
||||
setMargins(16.dp(context), 7.dp(context), 16.dp(context), 7.dp(context))
|
||||
}
|
||||
}
|
||||
|
||||
text = item.name
|
||||
}
|
||||
}
|
||||
|
||||
private class WCNetworksDiffCallback : BaseGroupedDiffCallback<WCNetworkListModel.Label, WCNetworkListModel.Chain>(WCNetworkListModel.Label::class.java) {
|
||||
|
||||
override fun areGroupItemsTheSame(oldItem: WCNetworkListModel.Label, newItem: WCNetworkListModel.Label): Boolean {
|
||||
return oldItem.name == newItem.name
|
||||
}
|
||||
|
||||
override fun areGroupContentsTheSame(oldItem: WCNetworkListModel.Label, newItem: WCNetworkListModel.Label): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun areChildItemsTheSame(oldItem: WCNetworkListModel.Chain, newItem: WCNetworkListModel.Chain): Boolean {
|
||||
return oldItem.chainUi.id == newItem.chainUi.id
|
||||
}
|
||||
|
||||
override fun areChildContentsTheSame(oldItem: WCNetworkListModel.Chain, newItem: WCNetworkListModel.Chain): Boolean {
|
||||
return oldItem.chainUi == newItem.chainUi
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.approve.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.BaseDynamicListBottomSheet
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
|
||||
class WCNetworksBottomSheet(
|
||||
context: Context,
|
||||
private val data: List<WCNetworkListModel>,
|
||||
) : BaseDynamicListBottomSheet(context) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.common_networks)
|
||||
|
||||
recyclerView.setHasFixedSize(true)
|
||||
|
||||
val adapter = WCNetworksAdapter()
|
||||
recyclerView.adapter = adapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.SessionDappMetadata
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.dAppTitle
|
||||
|
||||
interface WalletConnectSessionMapper {
|
||||
|
||||
fun formatSessionDAppTitle(metadata: SessionDappMetadata?): String
|
||||
}
|
||||
|
||||
class RealWalletConnectSessionMapper(
|
||||
private val resourceManager: ResourceManager
|
||||
) : WalletConnectSessionMapper {
|
||||
|
||||
override fun formatSessionDAppTitle(metadata: SessionDappMetadata?): String {
|
||||
return metadata?.dAppTitle ?: resourceManager.getString(R.string.wallet_connect_unknown_dapp)
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.common.view.showValueOrHide
|
||||
import io.novafoundation.nova.feature_account_api.view.showWallet
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainListBottomSheet
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.showChainsOverview
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.databinding.FragmentWcSessionDetailsBinding
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.di.WalletConnectFeatureComponent
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletConnectSessionDetailsFragment : BaseFragment<WalletConnectSessionDetailsViewModel, FragmentWcSessionDetailsBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PAYLOAD = "WalletConnectSessionsFragment.Payload"
|
||||
fun getBundle(payload: WalletConnectSessionDetailsPayload) = bundleOf(KEY_PAYLOAD to payload)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentWcSessionDetailsBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun initViews() {
|
||||
binder.wcSessionDetailsToolbar.setHomeButtonListener { viewModel.exit() }
|
||||
|
||||
binder.wcSessionDetailsDisconnect.setOnClickListener { viewModel.disconnect() }
|
||||
binder.wcSessionDetailsDisconnect.prepareForProgress(viewLifecycleOwner)
|
||||
binder.wcSessionDetailsNetworks.setOnClickListener { viewModel.networksClicked() }
|
||||
|
||||
binder.wcSessionDetailsStatus.showValue(getString(R.string.common_active))
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<WalletConnectFeatureComponent>(
|
||||
requireContext(),
|
||||
WalletConnectFeatureApi::class.java
|
||||
)
|
||||
.walletConnectSessionDetailsComponentFactory()
|
||||
.create(this, argument(KEY_PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: WalletConnectSessionDetailsViewModel) {
|
||||
viewModel.sessionUi.observe { sessionUi ->
|
||||
binder.wcSessionDetailsWallet.showWallet(sessionUi.wallet)
|
||||
binder.wcSessionDetailsDApp.showValueOrHide(sessionUi.dappUrl)
|
||||
binder.wcSessionDetailsNetworks.showChainsOverview(sessionUi.networksOverview)
|
||||
|
||||
binder.wcSessionDetailsTitle.text = sessionUi.dappTitle
|
||||
binder.wcSessionDetailsIcon.showDAppIcon(sessionUi.dappIcon, imageLoader)
|
||||
|
||||
with(sessionUi.status) {
|
||||
binder.wcSessionDetailsStatus.setImage(icon, sizeDp = 14)
|
||||
binder.wcSessionDetailsStatus.setPrimaryValueStyle(labelStyle)
|
||||
binder.wcSessionDetailsStatus.showValue(label)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.showChainBottomSheet.observeEvent { chainList ->
|
||||
ChainListBottomSheet(
|
||||
context = requireContext(),
|
||||
data = chainList
|
||||
).show()
|
||||
}
|
||||
|
||||
viewModel.disconnectButtonState.observe(binder.wcSessionDetailsDisconnect::setState)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class WalletConnectSessionDetailsPayload(val sessionTopic: String) : Parcelable
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.withFlagSet
|
||||
import io.novafoundation.nova.common.view.TableCellView.FieldStyle
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.formatChainListOverview
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSessionDetails
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common.WalletConnectSessionMapper
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.model.WalletConnectSessionDetailsUi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WalletConnectSessionDetailsViewModel(
|
||||
private val router: WalletConnectRouter,
|
||||
private val interactor: WalletConnectSessionInteractor,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val walletUiUseCase: WalletUiUseCase,
|
||||
private val walletConnectSessionMapper: WalletConnectSessionMapper,
|
||||
private val payload: WalletConnectSessionDetailsPayload,
|
||||
private val walletConnectSessionsUseCase: WalletConnectSessionsUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _showChainBottomSheet = MutableLiveData<Event<List<ChainUi>>>()
|
||||
val showChainBottomSheet: LiveData<Event<List<ChainUi>>> = _showChainBottomSheet
|
||||
|
||||
private val disconnectInProgressFlow = MutableStateFlow(false)
|
||||
|
||||
val disconnectButtonState = disconnectInProgressFlow.map { disconnectInProgress ->
|
||||
if (disconnectInProgress) {
|
||||
DescriptiveButtonState.Loading
|
||||
} else {
|
||||
DescriptiveButtonState.Enabled(resourceManager.getString(R.string.common_disconnect))
|
||||
}
|
||||
}.shareInBackground()
|
||||
|
||||
private val sessionFlow = interactor.activeSessionFlow(payload.sessionTopic)
|
||||
.shareInBackground()
|
||||
|
||||
val sessionUi = sessionFlow
|
||||
.filterNotNull()
|
||||
.map(::mapSessionDetailsToUi)
|
||||
.shareInBackground()
|
||||
|
||||
init {
|
||||
watchSessionDisconnect()
|
||||
}
|
||||
|
||||
fun exit() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun networksClicked() = launch {
|
||||
_showChainBottomSheet.value = sessionUi.first().networks.event()
|
||||
}
|
||||
|
||||
fun disconnect() = launch {
|
||||
val sessionTopic = sessionFlow.first()?.sessionTopic ?: return@launch
|
||||
|
||||
disconnectInProgressFlow.withFlagSet {
|
||||
interactor.disconnect(sessionTopic)
|
||||
.onFailure(::showError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchSessionDisconnect() {
|
||||
sessionFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { if (it == null) closeSessionsScreen() }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private suspend fun closeSessionsScreen() {
|
||||
val numberOfActiveSessions = walletConnectSessionsUseCase.activeSessionsNumber()
|
||||
|
||||
if (numberOfActiveSessions > 0) {
|
||||
router.back()
|
||||
} else {
|
||||
router.backToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun mapSessionDetailsToUi(session: WalletConnectSessionDetails): WalletConnectSessionDetailsUi {
|
||||
val chainUis = session.chains.map(::mapChainToUi)
|
||||
|
||||
return WalletConnectSessionDetailsUi(
|
||||
dappTitle = walletConnectSessionMapper.formatSessionDAppTitle(session.dappMetadata),
|
||||
dappUrl = session.dappMetadata?.dAppUrl,
|
||||
dappIcon = session.dappMetadata?.icon,
|
||||
networksOverview = resourceManager.formatChainListOverview(chainUis),
|
||||
networks = chainUis,
|
||||
wallet = walletUiUseCase.walletUiFor(session.connectedMetaAccount),
|
||||
status = mapSessionStatusToUi(session.status)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapSessionStatusToUi(status: WalletConnectSessionDetails.SessionStatus): WalletConnectSessionDetailsUi.SessionStatus {
|
||||
return when (status) {
|
||||
WalletConnectSessionDetails.SessionStatus.ACTIVE -> WalletConnectSessionDetailsUi.SessionStatus(
|
||||
label = resourceManager.getString(R.string.common_active),
|
||||
labelStyle = FieldStyle.POSITIVE,
|
||||
icon = R.drawable.ic_indicator_positive_pulse
|
||||
)
|
||||
WalletConnectSessionDetails.SessionStatus.EXPIRED -> WalletConnectSessionDetailsUi.SessionStatus(
|
||||
label = resourceManager.getString(R.string.common_expired),
|
||||
labelStyle = FieldStyle.SECONDARY,
|
||||
icon = R.drawable.ic_indicator_inactive_pulse
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.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_wallet_connect_impl.presentation.sessions.details.WalletConnectSessionDetailsFragment
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.WalletConnectSessionDetailsPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
WalletConnectSessionDetailsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface WalletConnectSessionDetailsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: WalletConnectSessionDetailsPayload,
|
||||
): WalletConnectSessionDetailsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: WalletConnectSessionDetailsFragment)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.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.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common.WalletConnectSessionMapper
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.WalletConnectSessionDetailsPayload
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.WalletConnectSessionDetailsViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class WalletConnectSessionDetailsModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(WalletConnectSessionDetailsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: WalletConnectRouter,
|
||||
walletConnectSessionMapper: WalletConnectSessionMapper,
|
||||
interactor: WalletConnectSessionInteractor,
|
||||
resourceManager: ResourceManager,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
payload: WalletConnectSessionDetailsPayload,
|
||||
walletConnectSessionsUseCase: WalletConnectSessionsUseCase
|
||||
): ViewModel {
|
||||
return WalletConnectSessionDetailsViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
resourceManager = resourceManager,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
walletConnectSessionMapper = walletConnectSessionMapper,
|
||||
payload = payload,
|
||||
walletConnectSessionsUseCase = walletConnectSessionsUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): WalletConnectSessionDetailsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(WalletConnectSessionDetailsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import io.novafoundation.nova.common.view.TableCellView
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletModel
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainListOverview
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
|
||||
class WalletConnectSessionDetailsUi(
|
||||
val dappTitle: String,
|
||||
val dappUrl: String?,
|
||||
val dappIcon: String?,
|
||||
val networksOverview: ChainListOverview,
|
||||
val networks: List<ChainUi>,
|
||||
val wallet: WalletModel,
|
||||
val status: SessionStatus
|
||||
) {
|
||||
|
||||
class SessionStatus(
|
||||
val label: String,
|
||||
val labelStyle: TableCellView.FieldStyle,
|
||||
@DrawableRes val icon: Int
|
||||
)
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class WalletConnectSessionsPayload(val metaId: Long?) : Parcelable
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.novafoundation.nova.common.list.BaseListAdapter
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.view.DAppView
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.R
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.model.SessionListModel
|
||||
|
||||
class WalletConnectSessionsAdapter(
|
||||
private val handler: Handler
|
||||
) : BaseListAdapter<SessionListModel, SessionHolder>(SessionDiffCallback()) {
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun itemClicked(item: SessionListModel)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionHolder {
|
||||
return SessionHolder(DAppView.createUsingMathParentWidth(parent.context), handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SessionHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
class SessionHolder(
|
||||
private val dAppView: DAppView,
|
||||
private val itemHandler: WalletConnectSessionsAdapter.Handler,
|
||||
) : BaseViewHolder(dAppView) {
|
||||
|
||||
override fun unbind() {
|
||||
dAppView.clearIcon()
|
||||
}
|
||||
|
||||
fun bind(item: SessionListModel) = with(dAppView) {
|
||||
setIconUrl(item.iconUrl)
|
||||
setTitle(item.dappTitle)
|
||||
setSubtitle(item.walletModel.name)
|
||||
enableSubtitleIcon().setImageDrawable(item.walletModel.icon)
|
||||
|
||||
setActionResource(iconRes = R.drawable.ic_chevron_right, colorRes = R.color.icon_secondary)
|
||||
|
||||
setOnClickListener { itemHandler.itemClicked(item) }
|
||||
}
|
||||
}
|
||||
|
||||
class SessionDiffCallback : DiffUtil.ItemCallback<SessionListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: SessionListModel, newItem: SessionListModel): Boolean {
|
||||
return oldItem.sessionTopic == newItem.sessionTopic
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SessionListModel, newItem: SessionListModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list
|
||||
|
||||
import android.util.Log
|
||||
import com.walletconnect.web3.wallet.client.Wallet
|
||||
import com.walletconnect.web3.wallet.client.Web3Wallet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
||||
sealed class WalletConnectSessionsEvent {
|
||||
|
||||
data class SessionProposal(val proposal: Wallet.Model.SessionProposal) : WalletConnectSessionsEvent()
|
||||
|
||||
data class SessionRequest(val request: Wallet.Model.SessionRequest) : WalletConnectSessionsEvent()
|
||||
|
||||
data class SessionSettlement(val settlement: Wallet.Model.SettledSessionResponse) : WalletConnectSessionsEvent()
|
||||
|
||||
data class SessionDeleted(val delete: Wallet.Model.SessionDelete) : WalletConnectSessionsEvent()
|
||||
}
|
||||
|
||||
fun Web3Wallet.sessionEventsFlow(scope: CoroutineScope): Flow<WalletConnectSessionsEvent> {
|
||||
return callbackFlow {
|
||||
setWalletDelegate(object : Web3Wallet.WalletDelegate {
|
||||
|
||||
override fun onAuthRequest(authRequest: Wallet.Model.AuthRequest, verifyContext: Wallet.Model.VerifyContext) {
|
||||
Log.d("WalletConnect", "Auth request: $authRequest")
|
||||
}
|
||||
|
||||
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
|
||||
Log.d("WalletConnect", "on connection state change: $state")
|
||||
}
|
||||
|
||||
override fun onError(error: Wallet.Model.Error) {
|
||||
Log.e("WalletConnect", "Wallet Connect error", error.throwable)
|
||||
}
|
||||
|
||||
override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {
|
||||
Log.d("WalletConnect", "Proposal expired: $proposal")
|
||||
}
|
||||
|
||||
override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) {
|
||||
Log.d("WalletConnect", "Request expired: $request")
|
||||
}
|
||||
|
||||
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
|
||||
Log.d("WalletConnect", "on session delete: $sessionDelete")
|
||||
channel.trySend(WalletConnectSessionsEvent.SessionDeleted(sessionDelete))
|
||||
}
|
||||
|
||||
override fun onSessionExtend(session: Wallet.Model.Session) {
|
||||
Log.d("WalletConnect", "On session extend: $session")
|
||||
}
|
||||
|
||||
override fun onSessionProposal(sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext) {
|
||||
Log.d("WalletConnect", "on session proposal: $sessionProposal")
|
||||
channel.trySend(WalletConnectSessionsEvent.SessionProposal(sessionProposal))
|
||||
}
|
||||
|
||||
override fun onSessionRequest(sessionRequest: Wallet.Model.SessionRequest, verifyContext: Wallet.Model.VerifyContext) {
|
||||
Log.d("WalletConnect", "on session request: $sessionRequest")
|
||||
channel.trySend(WalletConnectSessionsEvent.SessionRequest(sessionRequest))
|
||||
}
|
||||
|
||||
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
|
||||
Log.d("WalletConnect", "on session settled: $settleSessionResponse")
|
||||
channel.trySend(WalletConnectSessionsEvent.SessionSettlement(settleSessionResponse))
|
||||
}
|
||||
|
||||
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
|
||||
Log.d("WalletConnect", "on session update: $sessionUpdateResponse")
|
||||
}
|
||||
})
|
||||
|
||||
awaitClose { }
|
||||
}.shareIn(scope, SharingStarted.Eagerly)
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.databinding.FragmentWcSessionsBinding
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.di.WalletConnectFeatureComponent
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.model.SessionListModel
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletConnectSessionsFragment : BaseFragment<WalletConnectSessionsViewModel, FragmentWcSessionsBinding>(), WalletConnectSessionsAdapter.Handler {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PAYLOAD = "WalletConnectSessionsFragment.Payload"
|
||||
fun getBundle(payload: WalletConnectSessionsPayload) = bundleOf(KEY_PAYLOAD to payload)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentWcSessionsBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val sessionsAdapter = WalletConnectSessionsAdapter(handler = this)
|
||||
|
||||
override fun initViews() {
|
||||
binder.wcSessionsToolbar.setHomeButtonListener { viewModel.exit() }
|
||||
|
||||
binder.wcSessionsConnectionsList.setHasFixedSize(true)
|
||||
binder.wcSessionsConnectionsList.adapter = sessionsAdapter
|
||||
|
||||
binder.wcSessionsNewConnection.setOnClickListener { viewModel.newSessionClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<WalletConnectFeatureComponent>(
|
||||
requireContext(),
|
||||
WalletConnectFeatureApi::class.java
|
||||
)
|
||||
.walletConnectSessionsComponentFactory()
|
||||
.create(this, argument(KEY_PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: WalletConnectSessionsViewModel) {
|
||||
viewModel.sessionsFlow.observe { sessions ->
|
||||
sessionsAdapter.submitList(sessions)
|
||||
|
||||
binder.wcSessionsConnectionsList.setVisible(sessions.isNotEmpty())
|
||||
binder.wcSessionsConnectionsPlaceholder.setVisible(sessions.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun itemClicked(item: SessionListModel) {
|
||||
viewModel.sessionClicked(item)
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.model.WalletConnectSession
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common.WalletConnectSessionMapper
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.details.WalletConnectSessionDetailsPayload
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.model.SessionListModel
|
||||
|
||||
class WalletConnectSessionsViewModel(
|
||||
private val router: WalletConnectRouter,
|
||||
private val interactor: WalletConnectSessionInteractor,
|
||||
private val walletUiUseCase: WalletUiUseCase,
|
||||
private val walletConnectSessionMapper: WalletConnectSessionMapper,
|
||||
private val walletConnectSessionsPayload: WalletConnectSessionsPayload
|
||||
) : BaseViewModel() {
|
||||
|
||||
val sessionsFlow = interactor.activeSessionsFlow(walletConnectSessionsPayload.metaId)
|
||||
.mapList(::mapSessionToUi)
|
||||
.shareInBackground()
|
||||
|
||||
fun exit() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun newSessionClicked() {
|
||||
router.openScanPairingQrCode()
|
||||
}
|
||||
|
||||
fun sessionClicked(item: SessionListModel) {
|
||||
router.openSessionDetails(WalletConnectSessionDetailsPayload(item.sessionTopic))
|
||||
}
|
||||
|
||||
private suspend fun mapSessionToUi(session: WalletConnectSession): SessionListModel {
|
||||
val title = walletConnectSessionMapper.formatSessionDAppTitle(session.dappMetadata)
|
||||
|
||||
return SessionListModel(
|
||||
dappTitle = title,
|
||||
walletModel = walletUiUseCase.walletUiFor(session.connectedMetaAccount),
|
||||
iconUrl = session.dappMetadata?.icon,
|
||||
sessionTopic = session.sessionTopic
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.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_wallet_connect_impl.presentation.sessions.list.WalletConnectSessionsFragment
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.WalletConnectSessionsPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
WalletConnectSessionsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface WalletConnectSessionsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: WalletConnectSessionsPayload
|
||||
): WalletConnectSessionsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: WalletConnectSessionsFragment)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.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_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.WalletConnectRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.WalletConnectSessionInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.common.WalletConnectSessionMapper
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.WalletConnectSessionsPayload
|
||||
import io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.WalletConnectSessionsViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class WalletConnectSessionsModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(WalletConnectSessionsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: WalletConnectRouter,
|
||||
interactor: WalletConnectSessionInteractor,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
walletConnectSessionMapper: WalletConnectSessionMapper,
|
||||
walletConnectSessionsPayload: WalletConnectSessionsPayload
|
||||
): ViewModel {
|
||||
return WalletConnectSessionsViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
walletConnectSessionMapper = walletConnectSessionMapper,
|
||||
walletConnectSessionsPayload = walletConnectSessionsPayload
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): WalletConnectSessionsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(WalletConnectSessionsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_wallet_connect_impl.presentation.sessions.list.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletModel
|
||||
|
||||
data class SessionListModel(
|
||||
val dappTitle: String,
|
||||
val walletModel: WalletModel,
|
||||
val iconUrl: String?,
|
||||
val sessionTopic: String,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<io.novafoundation.nova.common.presentation.scan.ScanView
|
||||
android:id="@+id/walletConnectScan"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:subTitle="@string/wallet_connect_v2"
|
||||
app:title="@string/common_scan_qr_code" />
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
android:id="@+id/walletConnectScanToolbar"
|
||||
app:contentBackground="@android:color/transparent" />
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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/wcApproveSessionToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:homeButtonIcon="@drawable/ic_close"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:titleText="@string/wallet_connect_title" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wcApproveSessionNovaIcon"
|
||||
style="@style/Widget.Nova.Icon.Big"
|
||||
android:layout_marginTop="24dp"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_pezkuwi_logo"
|
||||
app:layout_constraintEnd_toStartOf="@+id/wcApproveSessionNovaArrow"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wcApproveSessionNovaArrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_bidirectonal"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/wcApproveSessionNovaIcon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/wcApproveSessionIcon"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/wcApproveSessionNovaIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/wcApproveSessionNovaIcon"
|
||||
app:tint="@color/icon_secondary" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wcApproveSessionIcon"
|
||||
style="@style/Widget.Nova.Icon.Big"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/wcApproveSessionNovaArrow"
|
||||
app:layout_constraintTop_toTopOf="@+id/wcApproveSessionNovaIcon"
|
||||
tools:src="@drawable/ic_earth" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wcApproveSessionTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Title3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/wcApproveSessionNovaIcon"
|
||||
tools:text="Allow “Polkadot.js” to access your account addresses?" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wcApproveSessionSubTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/dapp_confirm_authorize_subtitle"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/wcApproveSessionTitle" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:id="@+id/wcApproveSessionRequestSession"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wcApproveSessionSubTitle">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/wcApproveSessionDApp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/dapp_dapp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/wcApproveSessionNetworks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_networks" />
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<io.novafoundation.nova.common.view.AlertView
|
||||
android:id="@+id/wcApproveSessionChainsAlert"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wcApproveSessionRequestSession"
|
||||
tools:text="Some of the required networks requested by 1inch are not supported in Nova Wallet" />
|
||||
|
||||
<io.novafoundation.nova.feature_account_api.view.AccountView
|
||||
android:id="@+id/wcApproveSessionWallet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:actionIcon="@drawable/ic_chevron_right"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wcApproveSessionChainsAlert"
|
||||
app:showBackground="true" />
|
||||
|
||||
<io.novafoundation.nova.common.view.AlertView
|
||||
android:id="@+id/wcApproveSessionAccountsAlert"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wcApproveSessionWallet"
|
||||
tools:text="Ethereum account is missing. Add account to the wallet in Settings" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/wcApproveSessionReject"
|
||||
style="@style/Widget.Nova.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/common_reject"
|
||||
app:appearance="secondary" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/wcApproveSessionAllow"
|
||||
style="@style/Widget.Nova.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/common_allow"
|
||||
app:appearance="primary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,84 @@
|
||||
<?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:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/wcSessionDetailsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:titleText="@string/wallet_connect_title" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wcSessionDetailsIcon"
|
||||
style="@style/Widget.Nova.Icon.Big"
|
||||
android:layout_marginTop="32dp"
|
||||
tools:src="@drawable/ic_earth" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wcSessionDetailsTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Title3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="1inch dApp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/wcSessionDetailsWallet"
|
||||
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/wcSessionDetailsDApp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/dapp_dapp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/wcSessionDetailsNetworks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_networks" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/wcSessionDetailsStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueStyle="positive"
|
||||
app:title="@string/common_status" />
|
||||
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/wcSessionDetailsDisconnect"
|
||||
style="@style/Widget.Nova.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/common_disconnect"
|
||||
app:appearance="primaryNegative" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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/wcSessionsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:titleText="@string/wallet_connect_title" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PlaceholderView
|
||||
android:id="@+id/wcSessionsConnectionsPlaceholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/wallet_connect_sessions_placeholder"
|
||||
android:visibility="gone"
|
||||
app:image="@drawable/ic_placeholder"
|
||||
app:placeholderBackgroundStyle="no_background"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/wcSessionsConnectionsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/wcSessionsNewConnection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:iconSrc="@drawable/ic_qr_scan"
|
||||
android:text="@string/wallet_connect_new_connection" />
|
||||
</LinearLayout>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:background="@color/secondary_screen_background"
|
||||
android:textColor="@color/text_secondary"
|
||||
tools:text="@string/common_required"
|
||||
android:layout_marginStart="16dp"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:id="@+id/itemWcNetworksLabel"
|
||||
android:layout_height="wrap_content" />
|
||||
Reference in New Issue
Block a user