Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-07 18:18:27 +07:00
parent fc8fa8ff8d
commit bcc957e249
34 changed files with 898 additions and 6 deletions

View file

@ -100,6 +100,7 @@ dependencies {
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.staking)
implementation(projects.domain.walletConnect)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.markets)
implementation(projects.domain.manageTokens)
implementation(projects.domain.nft)

View file

@ -8,9 +8,13 @@ import com.reown.android.relay.ConnectionType
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.unsupportedDApps
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
@ -555,9 +559,4 @@ internal class DefaultLegacyWalletConnectRepository(
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return wcProvidedChains.intersect(userChains.toSet())
}
private companion object {
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
}
}

View file

@ -13,6 +13,7 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.walletConnect)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.wallets.models)
/* Project - Data */
@ -25,6 +26,11 @@ dependencies {
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/* Reown - WalletConnect */
implementation(deps.reownCore)
implementation(deps.reownWeb3)
/* Other */
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
}

View file

@ -0,0 +1,38 @@
package com.tangem.data.walletconnect
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import timber.log.Timber
internal class DefaultWcSessionsManager : WcSessionsManager, WcSdkObserver {
private val _sessions = MutableSharedFlow<Map<UserWalletId, List<WcSession>>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val sessions get() = _sessions.distinctUntilChanged()
override suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) {
TODO("Not yet implemented")
}
override suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) {
TODO("Not yet implemented")
}
override suspend fun findSessionByTopic(topic: String): WcSession? {
TODO("Not yet implemented")
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
// Triggered when the session is deleted by the peer
Timber.i("onSessionDelete: $sessionDelete")
// todo (wc) find session, delete and update sessions Flow
}
}

View file

@ -0,0 +1,115 @@
package com.tangem.data.walletconnect.initialize
import android.app.Application
import com.reown.android.Core
import com.reown.android.CoreClient
import com.reown.android.relay.ConnectionType
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.DefaultWcSessionsManager
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.request.DefaultWcRequestService
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import timber.log.Timber
internal class DefaultWcInitializeUseCase(
private val application: Application,
private val sessionsManager: DefaultWcSessionsManager,
private val networkService: DefaultWcRequestService,
private val wcPairFlow: DefaultWcPairUseCase,
) : WcInitializeUseCase {
private val wcSdkObservers = mutableSetOf<WcSdkObserver>(
sessionsManager,
networkService,
wcPairFlow,
)
override fun init(projectId: String) {
val relayUrl = "relay.walletconnect.com"
val serverUrl = "wss://$relayUrl?projectId=$projectId"
val connectionType = ConnectionType.AUTOMATIC
val appMetaData = Core.Model.AppMetaData(
name = "Tangem",
description = "Tangem Wallet",
url = "tangem.com",
icons = listOf(
"https://user-images.githubusercontent.com/24321494/124071202-72a00900-da58-11eb-935a-dcdab21de52b.png",
),
redirect = "kotlin-wallet-wc:/request", // Custom Redirect URI
)
CoreClient.initialize(
relayServerUrl = serverUrl,
connectionType = connectionType,
application = application,
metaData = appMetaData,
) { error ->
Timber.e("Error while initializing client: $error")
}
WalletKit.initialize(
Wallet.Params.Init(core = CoreClient),
onSuccess = {
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")
},
)
}
private fun defineWalletDelegate() = object : WalletKit.WalletDelegate {
override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)?
get() = super.onSessionAuthenticate
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
wcSdkObservers.forEach { it.onConnectionStateChange(state) }
}
override fun onError(error: Wallet.Model.Error) {
wcSdkObservers.forEach { it.onError(error) }
}
override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {
wcSdkObservers.forEach { it.onProposalExpired(proposal) }
}
override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) {
wcSdkObservers.forEach { it.onRequestExpired(request) }
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) }
}
override fun onSessionExtend(session: Wallet.Model.Session) {
wcSdkObservers.forEach { it.onSessionExtend(session) }
}
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) }
}
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) }
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) }
}
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) }
}
}
}

View file

@ -0,0 +1,216 @@
package com.tangem.data.walletconnect.pair
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.toOurModel
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
@Suppress("UnusedPrivateMember") // todo(wc) remove after add mapping
internal class DefaultWcPairUseCase(
private val sessionsManager: WcSessionsManager,
) : WcPairUseCase, WcSdkObserver {
private val onWalletSelect = Channel<UserWallet>()
private val onAccountSelect = Channel<Any>()
private val onCallTerminalAction = Channel<TerminalAction>()
private val onSessionProposal =
Channel<Pair<Wallet.Model.SessionProposal, Wallet.Model.VerifyContext>>(Channel.BUFFERED)
private val onSessionSettleResponse = Channel<Wallet.Model.SettledSessionResponse>(Channel.BUFFERED)
override fun pairFlow(uri: String, source: WcPairUseCase.Source, selectedWallet: UserWallet): Flow<WcPairState> =
flow {
emit(WcPairState.Loading)
// call sdk.pair and wait result, finish flow on error
walletKitPair(uri).onLeft { throwable ->
emit(WcPairState.Error(throwable))
return@flow
}
// wait for sdk onSessionProposal callback
val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow().map { (fist, second) ->
val ourCopy = WcSdkSessionProposal(
name = fist.name,
description = fist.description,
url = fist.url,
proposerPublicKey = fist.proposerPublicKey,
)
ourCopy to second
}.first { (sessionProposal, verifyContext) ->
true // todo(wc) check verifyContext? compare uri?
}
// check unsupported dApps, just local constant for now, finish if unsupported
if (sdkSessionProposal.name in unsupportedDApps) {
Timber.i("Unsupported DApp")
val error = WcPairState
.Error(RuntimeException("todo(wc) use domain exception")) // todo(wc) WalletConnectError.UnsupportedDApp
emit(error)
return@flow
}
// flow that collect terminal and all middle actions
val actionsFlow = channelFlow<TerminalAction> {
val userWallet = onWalletSelect.receiveAsFlow()
.stateIn(scope = this, started = SharingStarted.Lazily, initialValue = selectedWallet)
val accountSelect = onAccountSelect.receiveAsFlow()
.stateIn(scope = this, started = SharingStarted.Lazily, initialValue = Any())
// combine all middle variables and emit new ProposalState to main FlowCollector
combine(accountSelect, userWallet) { account, selectedWallet ->
buildProposalState(sdkSessionProposal, verifyContext, account, selectedWallet)
}.onEach { newProposalState -> this@flow.emit(newProposalState) }
.launchIn(this)
// collect terminal action and emit to this inner channelFlow, unlike combine above
onCallTerminalAction.receiveAsFlow()
.onEach { this.channel.send(it) }
.launchIn(this)
}
// wait first terminal action and continue WC pair flow
val sessionForApprove: WcSessionProposal = when (val terminalAction = actionsFlow.first()) {
is TerminalAction.Approve -> terminalAction.sessionForApprove
TerminalAction.Reject -> {
// non suspending WalletKit.rejectSession call
rejectSession(sdkSessionProposal)
return@flow
}
}
// start flow of approving in wc sdk
emit(WcPairState.Approving.Loading(sessionForApprove))
fun mapResult(either: Either<Throwable, WcSession>) =
WcPairState.Approving.Result(sessionForApprove, either)
// call sdk approve and wait result
walletKitApproveSession(sessionForApprove).onLeft { emit(mapResult(it.left())) }.onRight {
// wait for sdk callback
val result = when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) {
is Wallet.Model.SettledSessionResponse.Error -> mapResult(
// WalletConnectError.ExternalApprovalError(settledSession.errorMessage) todo(wc)
RuntimeException("todo(wc) use domain exception").left(),
)
is Wallet.Model.SettledSessionResponse.Result -> {
val newSession = settledSession.session.toDomain(sessionForApprove.wallet)
sessionsManager.saveSessions(sessionForApprove.wallet.walletId, newSession)
mapResult(newSession.right())
}
}
emit(result)
}
}
override fun onWalletSelect(selectedWallet: UserWallet) {
onWalletSelect.trySend(selectedWallet)
}
override fun onAccountSelect(account: Any) {
onAccountSelect.trySend(account)
}
override fun approve(sessionForApprove: WcSessionProposal) {
onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove))
}
override fun reject() {
onCallTerminalAction.trySend(TerminalAction.Reject)
}
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.i("sessionProposal: $sessionProposal")
onSessionProposal.trySend(sessionProposal to verifyContext)
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
// Triggered when wallet receives the session settlement response from Dapp
Timber.i("onSessionSettleResponse: $settleSessionResponse")
onSessionSettleResponse.trySend(settleSessionResponse)
}
private suspend fun walletKitPair(uri: String): Either<Throwable, Unit> =
suspendCancellableCoroutine { continuation ->
WalletKit.pair(
params = Wallet.Params.Pair(uri),
onSuccess = {
Timber.i("Paired successfully: $it")
continuation.resume(Unit.right())
},
onError = {
Timber.e("Error while pairing: $it")
continuation.resume(it.throwable.left())
},
)
}
private suspend fun walletKitApproveSession(sessionForApprove: WcSessionProposal): Either<Throwable, Unit> =
suspendCancellableCoroutine { continuation ->
WalletKit.approveSession(
params = TODO("wc sdk model"),
onSuccess = {
Timber.i("Approved successfully: $it")
continuation.resume(Unit.right())
},
onError = {
Timber.e("Error while approving: $it")
continuation.resume(it.throwable.left())
},
)
}
private fun rejectSession(sessionProposal: WcSdkSessionProposal) {
WalletKit.rejectSession(
params = Wallet.Params.SessionReject(
proposerPublicKey = sessionProposal.proposerPublicKey,
reason = "",
),
onSuccess = {
Timber.i("Rejected successfully: $it")
},
onError = {
Timber.e("Error while rejecting: $it")
},
)
}
private fun buildProposalState(
sessionProposal: WcSdkSessionProposal,
verifyContext: Wallet.Model.VerifyContext,
selectedAccount: Any,
userWallet: UserWallet,
): WcPairState.Proposal {
// todo(wc) a lot of mapping from Wallet.Model.SessionProposal to our Blockchain, Network, ect
// todo(wc) check security status by Wallet.Model.VerifyContext or Blockaid, don't know for now
val mock: WcSessionProposal = TODO()
return WcPairState.Proposal(mock, Any())
}
private fun Wallet.Model.Session.toDomain(userWallet: UserWallet): WcSession = WcSession(
userWalletId = userWallet.walletId,
sdkModel = this.toOurModel(),
)
private sealed interface TerminalAction {
data class Approve(val sessionForApprove: WcSessionProposal) : TerminalAction
data object Reject : TerminalAction
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.data.walletconnect.request
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.toOurModel
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.request.WcRequestHandler
import com.tangem.domain.walletconnect.request.WcRequestService
import com.tangem.domain.walletconnect.respond.WcRespondService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
internal class DefaultWcRequestService(
private val sessionsManager: WcSessionsManager,
private val respondService: WcRespondService,
private val requestAdapters: List<WcRequestHandler<WcMethod>>,
private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope
) : WcRequestService, WcSdkObserver {
override val requests: MutableSharedFlow<WcRequest<*>> = MutableSharedFlow()
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
val sr = sessionRequest.toOurModel()
val method = sr.request.method
val params = sr.request.params
scope.launch {
val session = sessionsManager.findSessionByTopic(sr.topic)
val handler: WcRequestHandler<WcMethod>? = requestAdapters.firstOrNull { it.canHandle(method) }
val deserialized: WcMethod? = handler?.deserialize(method, params)
if (handler == null || deserialized == null || session == null) {
respondService.rejectRequest(sr, "UnsupportedMethod") // todo(wc) use our domain error
return@launch
}
val wcRequest = WcRequest(sr, session, deserialized)
handler.handle(wcRequest)
requests.emit(wcRequest)
}
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.data.walletconnect.respond
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.respond.WcRespondService
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
internal class DefaultWcRespondService : WcRespondService {
override suspend fun respond(request: WcSdkSessionRequest, response: String): Either<Throwable, Unit> =
suspendCancellableCoroutine { continuation ->
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = request.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = request.request.id,
result = response,
),
),
onSuccess = {
continuation.resume(Unit.right())
},
onError = {
continuation.resume(it.throwable.left())
},
)
}
override suspend fun rejectRequest(request: WcSdkSessionRequest, message: String) =
suspendCancellableCoroutine { continuation ->
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = request.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
id = request.request.id,
code = 0,
message = message,
),
),
onSuccess = {
continuation.resume(Unit.right())
},
onError = {
continuation.resume(it.throwable.left())
},
)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.data.walletconnect.utils
import com.reown.walletkit.client.Wallet
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest
internal fun Wallet.Model.Session.toOurModel(): WcSdkSession = WcSdkSession(
topic = topic,
)
internal fun Wallet.Model.SessionRequest.toOurModel(): WcSdkSessionRequest = WcSdkSessionRequest(
topic = this.topic,
chainId = this.chainId,
request = this.request.toOurModel(),
)
internal fun Wallet.Model.SessionRequest.JSONRPCRequest.toOurModel(): JSONRPCRequest = JSONRPCRequest(
id = this.id,
method = this.method,
params = this.params,
)

View file

@ -0,0 +1,38 @@
package com.tangem.data.walletconnect.utils
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
internal interface WcSdkObserver : WalletKit.WalletDelegate {
override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)?
get() = super.onSessionAuthenticate
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {}
override fun onError(error: Wallet.Model.Error) {}
override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {}
override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) {}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {}
override fun onSessionExtend(session: Wallet.Model.Session) {}
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
}
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {}
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {}
}

View file

@ -7,4 +7,8 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.wallets.models)
implementation(projects.domain.walletConnect.models)
/* Other */
implementation(deps.moshi.adapters)
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,10 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
/* Domain */
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.walletconnect.model
interface WcMethod {
data object Unsupported : WcMethod
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
data class WcRequest<M : WcMethod>(
val rawSdkRequest: WcSdkSessionRequest,
val session: WcSession,
val method: M,
)

View file

@ -0,0 +1,9 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.wallets.models.UserWalletId
data class WcSession(
val userWalletId: UserWalletId,
val sdkModel: WcSdkSession,
)

View file

@ -0,0 +1,15 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal
import com.tangem.domain.wallets.models.UserWallet
data class WcSessionProposal(
val dAppInfo: WcSdkSessionProposal,
// todo(wc) should map for all user wallets?
val wallet: UserWallet,
val missingChains: List<Network>,
val availableChains: List<Network>,
val notAddedChains: List<Network>,
)

View file

@ -0,0 +1,8 @@
package com.tangem.domain.walletconnect.model.sdkcopy
/**
* copy of [com.reown.walletkit.client.Wallet.Model.Session]
*/
data class WcSdkSession(
val topic: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.domain.walletconnect.model.sdkcopy
/**
* copy of [com.reown.walletkit.client.Wallet.Model.SessionProposal]
*/
data class WcSdkSessionProposal(
val name: String,
val description: String,
val url: String,
val proposerPublicKey: String,
)

View file

@ -0,0 +1,17 @@
package com.tangem.domain.walletconnect.model.sdkcopy
/**
* copy of [com.reown.walletkit.client.Wallet.Model.SessionRequest]
*/
data class WcSdkSessionRequest(
val topic: String,
val chainId: String?,
val request: JSONRPCRequest,
) {
data class JSONRPCRequest(
val id: Long,
val method: String,
val params: String,
)
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.walletconnect.repository
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface WcSessionsManager {
val sessions: Flow<Map<UserWalletId, List<WcSession>>>
suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession)
suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession)
suspend fun findSessionByTopic(topic: String): WcSession?
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.walletconnect.request
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
interface WcRequestHandler<M : WcMethod> {
fun canHandle(methodName: String): Boolean
fun deserialize(methodName: String, params: String): M?
fun handle(wcRequest: WcRequest<M>)
companion object {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> fromJson(params: String, moshi: Moshi): T? =
runCatching { moshi.adapter<T>().fromJsonValue(params) }.getOrNull()
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.walletconnect.request
import com.tangem.domain.walletconnect.model.WcRequest
import kotlinx.coroutines.flow.Flow
interface WcRequestService {
val requests: Flow<WcRequest<*>>
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.walletconnect.respond
import arrow.core.Either
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
interface WcRespondService {
suspend fun respond(request: WcSdkSessionRequest, response: String): Either<Throwable, Unit>
suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either<Throwable, Unit>
}

View file

@ -0,0 +1,25 @@
package com.tangem.domain.walletconnect.usecase
import arrow.core.Either
import kotlinx.coroutines.flow.Flow
interface WcSimpleSignUseCase<SignModel, MiddleAction> : WcUseCase {
fun signFlow(initModel: SignModel): Flow<State<SignModel>>
fun action(action: MiddleAction)
fun cancel()
fun sign(toSign: SignModel)
sealed interface State<SignModel> {
val model: SignModel
data class PreSign<SignModel>(override val model: SignModel) : State<SignModel>
data class Signing<SignModel>(override val model: SignModel) : State<SignModel>
data class Result<SignModel>(
val result: Either<Throwable, Unit>,
override val model: SignModel,
) : State<SignModel>
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.domain.walletconnect.usecase
interface WcUseCase

View file

@ -0,0 +1,7 @@
package com.tangem.domain.walletconnect.usecase
import kotlinx.coroutines.flow.Flow
interface WcUseCasesFlowProvider {
val flow: Flow<WcUseCase>
}

View file

@ -0,0 +1,72 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.respond.WcRespondService
import com.tangem.domain.walletconnect.usecase.WcUseCase
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
class EthPersonalSignUseCase(
private val wcRequest: WcRequest<WcEthMethod.SignMessage>,
private val respondService: WcRespondService,
) : WcUseCase {
private val onCallTerminalAction = Channel<TerminalAction>()
fun signFlow(): Flow<State> = flow {
val model = SignModel(wcRequest.method.raw)
emit(State.PreSign(model))
when (val action = onCallTerminalAction.receiveAsFlow().first()) {
TerminalAction.Cancel -> {
val result = respondService.rejectRequest(wcRequest.rawSdkRequest)
emit(State.Result(result, model))
}
is TerminalAction.Sign -> {
emit(State.Signing(action.toSign))
val signed = signAndPrepareForSend()
val result =
if (signed != null) {
respondService.respond(wcRequest.rawSdkRequest, signed)
} else {
respondService.rejectRequest(wcRequest.rawSdkRequest)
}
emit(State.Result(result, model))
}
}
}
@Suppress("FunctionOnlyReturningConstant") // todo(wc) remove later
private suspend fun signAndPrepareForSend(): String? {
return null // todo(wc)
}
fun sign(toSign: SignModel) {
onCallTerminalAction.trySend(TerminalAction.Sign(toSign))
}
fun cancel() {
onCallTerminalAction.trySend(TerminalAction.Cancel)
}
sealed interface TerminalAction {
data class Sign(val toSign: SignModel) : TerminalAction
data object Cancel : TerminalAction
}
sealed interface State {
val model: SignModel
data class PreSign(override val model: SignModel) : State
data class Signing(override val model: SignModel) : State
data class Result(
val result: Either<Throwable, Unit>,
override val model: SignModel,
) : State
}
data class SignModel(
val raw: List<String>,
)
}

View file

@ -0,0 +1,53 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.squareup.moshi.Moshi
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.request.WcRequestHandler
import com.tangem.domain.walletconnect.respond.WcRespondService
import com.tangem.domain.walletconnect.usecase.WcUseCase
import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider
import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
@Suppress("UnusedPrivateClass") // todo(wc) remove later
private class WcEthChain(
val moshi: Moshi,
private val respondService: WcRespondService,
) : WcRequestHandler<WcEthMethod>, WcUseCasesFlowProvider {
private val _flow: Channel<WcUseCase> = Channel(Channel.BUFFERED)
override val flow = _flow.receiveAsFlow()
override fun canHandle(methodName: String): Boolean {
return Name.entries.find { it.raw == methodName } != null
}
override fun deserialize(methodName: String, params: String): WcEthMethod? {
val name = Name.entries.find { it.raw == methodName } ?: return null
return when (name) {
Name.Sign -> TODO()
Name.PersonalSign -> WcRequestHandler.fromJson<SignMessage>(params, moshi)
Name.SignTypeData -> TODO()
Name.SignTypeDataV4 -> TODO()
Name.SignTransaction -> TODO()
Name.SendTransaction -> TODO()
}
}
override fun handle(wcRequest: WcRequest<WcEthMethod>) {
val useCase = when (wcRequest.method) {
is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest<SignMessage>, respondService)
}
_flow.trySend(useCase)
}
enum class Name(val raw: String) {
Sign("eth_sign"),
PersonalSign("personal_sign"),
SignTypeData("eth_signTypedData"),
SignTypeDataV4("eth_signTypedData_v4"),
SignTransaction("eth_signTransaction"),
SendTransaction("eth_sendTransaction"),
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.domain.walletconnect.model.WcMethod
sealed interface WcEthMethod : WcMethod {
data class SignMessage(
val raw: List<String>,
) : WcEthMethod
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.walletconnect.usecase.initialize
interface WcInitializeUseCase {
fun init(projectId: String)
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.walletconnect.usecase.pair
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionProposal
sealed interface WcPairState {
data object Loading : WcPairState
data class Error(val throwable: Throwable) : WcPairState
data class Proposal(
val dAppSession: WcSessionProposal,
val securityStatus: Any,
) : WcPairState
sealed interface Approving : WcPairState {
val session: WcSessionProposal
data class Loading(override val session: WcSessionProposal) : Approving
data class Result(
override val session: WcSessionProposal,
val result: Either<Throwable, WcSession>,
) : Approving
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.walletconnect.usecase.pair
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow
interface WcPairUseCase {
fun pairFlow(uri: String, source: Source, selectedWallet: UserWallet): Flow<WcPairState>
// todo(wc) should map for all user wallets without this action in domain layer?
fun onWalletSelect(selectedWallet: UserWallet)
// todo(wc) accounts in future?
fun onAccountSelect(account: Any)
fun approve(sessionForApprove: WcSessionProposal)
fun reject()
enum class Source { QR, DEEPLINK, ETC }
}

View file

@ -270,6 +270,7 @@ include(":domain:qr-scanning:models")
include(":domain:staking")
include(":domain:staking:models")
include(":domain:wallet-connect")
include(":domain:wallet-connect:models")
include(":domain:markets")
include(":domain:markets:models")
include(":domain:manage-tokens")