diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8bee4d929a..b7a7f549c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 0f673b9ba6..982bcd4438 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -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") - } } \ No newline at end of file diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 5dc33697df..7dbf3dc44a 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt new file mode 100644 index 0000000000..388c5c30a1 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt @@ -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>>( + 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 + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt new file mode 100644 index 0000000000..c7e39cd519 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -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( + 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) } + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt new file mode 100644 index 0000000000..89099dc766 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -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() + private val onAccountSelect = Channel() + private val onCallTerminalAction = Channel() + private val onSessionProposal = + Channel>(Channel.BUFFERED) + private val onSessionSettleResponse = Channel(Channel.BUFFERED) + + override fun pairFlow(uri: String, source: WcPairUseCase.Source, selectedWallet: UserWallet): Flow = + 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 { + 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) = + 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 = + 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 = + 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 + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt new file mode 100644 index 0000000000..c36cfbdb75 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -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>, + private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope +) : WcRequestService, WcSdkObserver { + + override val requests: MutableSharedFlow> = 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? = 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) + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt new file mode 100644 index 0000000000..7381e9b548 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -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 = + 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()) + }, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt new file mode 100644 index 0000000000..5da50ba3c4 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt @@ -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, +) \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt new file mode 100644 index 0000000000..5c15ea1b80 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt @@ -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) {} +} \ No newline at end of file diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index af72640fab..9b0cbaa2e3 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/domain/wallet-connect/models/.gitignore b/domain/wallet-connect/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/wallet-connect/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts new file mode 100644 index 0000000000..47fbf86d0b --- /dev/null +++ b/domain/wallet-connect/models/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} +dependencies { + + /* Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt new file mode 100644 index 0000000000..05c3df4d98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.walletconnect.model + +interface WcMethod { + data object Unsupported : WcMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt new file mode 100644 index 0000000000..256aa09ce1 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +data class WcRequest( + val rawSdkRequest: WcSdkSessionRequest, + val session: WcSession, + val method: M, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt new file mode 100644 index 0000000000..92501d8bba --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -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, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt new file mode 100644 index 0000000000..45c189cf20 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -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, + val availableChains: List, + val notAddedChains: List, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt new file mode 100644 index 0000000000..bd88f8ea98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt @@ -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, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt new file mode 100644 index 0000000000..93a3734601 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt @@ -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, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt new file mode 100644 index 0000000000..7b2ed2ff37 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt @@ -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, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt new file mode 100644 index 0000000000..4d2d837da0 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -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>> + suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun findSessionByTopic(topic: String): WcSession? +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt new file mode 100644 index 0000000000..410815ae03 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt @@ -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 { + fun canHandle(methodName: String): Boolean + fun deserialize(methodName: String, params: String): M? + fun handle(wcRequest: WcRequest) + + companion object { + @OptIn(ExperimentalStdlibApi::class) + inline fun fromJson(params: String, moshi: Moshi): T? = + runCatching { moshi.adapter().fromJsonValue(params) }.getOrNull() + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt new file mode 100644 index 0000000000..5918accfd9 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt @@ -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> +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt new file mode 100644 index 0000000000..c74ce0e017 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt @@ -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 + suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt new file mode 100644 index 0000000000..e0bb5239a4 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.walletconnect.usecase + +import arrow.core.Either +import kotlinx.coroutines.flow.Flow + +interface WcSimpleSignUseCase : WcUseCase { + + fun signFlow(initModel: SignModel): Flow> + + fun action(action: MiddleAction) + + fun cancel() + fun sign(toSign: SignModel) + + 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, + override val model: SignModel, + ) : State + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt new file mode 100644 index 0000000000..a2efe639cd --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.walletconnect.usecase + +interface WcUseCase \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt new file mode 100644 index 0000000000..700cf1865c --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.walletconnect.usecase + +import kotlinx.coroutines.flow.Flow + +interface WcUseCasesFlowProvider { + val flow: Flow +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt new file mode 100644 index 0000000000..5e86f84006 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt @@ -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, + private val respondService: WcRespondService, +) : WcUseCase { + + private val onCallTerminalAction = Channel() + + fun signFlow(): Flow = 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, + override val model: SignModel, + ) : State + } + + data class SignModel( + val raw: List, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt new file mode 100644 index 0000000000..ab18902b6e --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt @@ -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, WcUseCasesFlowProvider { + + private val _flow: Channel = 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(params, moshi) + Name.SignTypeData -> TODO() + Name.SignTypeDataV4 -> TODO() + Name.SignTransaction -> TODO() + Name.SendTransaction -> TODO() + } + } + + override fun handle(wcRequest: WcRequest) { + val useCase = when (wcRequest.method) { + is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest, 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"), + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt new file mode 100644 index 0000000000..cf40981044 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt @@ -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, + ) : WcEthMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt new file mode 100644 index 0000000000..71f465a43f --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.walletconnect.usecase.initialize + +interface WcInitializeUseCase { + fun init(projectId: String) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt new file mode 100644 index 0000000000..1472db789e --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt @@ -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, + ) : Approving + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt new file mode 100644 index 0000000000..4bf3e9b047 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -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 + + // 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 } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c8b22d2a48..cda72ef1f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")