Updated on 2026-08-14
This commit is contained in:
parent
fc8fa8ff8d
commit
bcc957e249
34 changed files with 898 additions and 6 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
1
domain/wallet-connect/models/.gitignore
vendored
Normal file
1
domain/wallet-connect/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
10
domain/wallet-connect/models/build.gradle.kts
Normal file
10
domain/wallet-connect/models/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.walletconnect.model
|
||||
|
||||
interface WcMethod {
|
||||
data object Unsupported : WcMethod
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<*>>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.domain.walletconnect.usecase
|
||||
|
||||
interface WcUseCase
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.domain.walletconnect.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WcUseCasesFlowProvider {
|
||||
val flow: Flow<WcUseCase>
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.walletconnect.usecase.initialize
|
||||
|
||||
interface WcInitializeUseCase {
|
||||
fun init(projectId: String)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue