Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 21:31:52 +04:00
commit 3aaf9c5b0a
2074 changed files with 88068 additions and 12659 deletions

View file

@ -63,7 +63,7 @@ dependencies {
/* Tests */
testImplementation(projects.common.test)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.turbine)
}

View file

@ -7,22 +7,10 @@
<ID>BooleanPropertyNaming:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$val haveEmptySessions = emptyNetworkSessions.isNotEmpty()</ID>
<ID>BooleanPropertyNaming:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$val haveSomeUnknown = unknownStoredSessions.isNotEmpty()</ID>
<ID>BooleanPropertyNaming:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$val haveSomeUnknownSdkSessions = unknownSdkSessions.isNotEmpty()</ID>
<ID>BooleanPropertyNaming:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$val someMigrate = migrateToAccountSession(inStore)</ID>
<ID>BooleanPropertyNaming:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)</ID>
<ID>BooleanPropertyNaming:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$var someMigrated = false</ID>
<ID>CastNullableToNonNullableType:WcEthSendTransactionUseCase.kt$WcEthSendTransactionUseCase$as</ID>
<ID>CastNullableToNonNullableType:WcEthSignTransactionUseCase.kt$WcEthSignTransactionUseCase$as</ID>
<ID>MultilineLambdaItParameter:BlockAidVerificationDelegate.kt$BlockAidVerificationDelegate${ Timber.e("Failed to verify transaction: ${it.localizedMessage}") emit(Lce.Error(it)) }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest") analytics.send( WcAnalyticEvents.PairFailed( errorCode = it.code, errorMessage = it.message, ), ) emit(WcPairState.Error(it)) }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ Timber.tag(WC_TAG).e(it, "Failed to verify DApp ${sessionProposal.name}") CheckDAppResult.FAILED_TO_VERIFY }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ analytics.send( WcAnalyticEvents.DAppConnectionFailed( errorCode = it.code, errorMessage = it.message, ), ) sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ analytics.send( WcAnalyticEvents.PairFailed( errorCode = it.code, errorMessage = it.message, ), ) emit(WcPairState.Error(it)) }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ if (it != null) { Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") } else { Timber.tag(WC_TAG).i("Completed successfully $pairRequest") } }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ val pairError: WcPairError = when (it) { is TimeoutCancellationException -&gt; WcPairError.TimeoutException(it.message.orEmpty()) else -&gt; WcPairError.Unknown(it.message.orEmpty()) } emit(WcPairState.Error(pairError)) }</ID>
<ID>MultilineLambdaItParameter:DefaultWcPairUseCase.kt$DefaultWcPairUseCase${ when (it) { is WcPairError -&gt; it.left() else -&gt; WcPairError.Unknown(it.localizedMessage.orEmpty()).left() } }</ID>
<ID>MultilineLambdaItParameter:DefaultWcRequestUseCaseFactory.kt$DefaultWcRequestUseCaseFactory${ Timber.tag(WC_TAG).e("$it") it.left() }</ID>
<ID>MultilineLambdaItParameter:DefaultWcRespondService.kt$DefaultWcRespondService${ if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") continuation.resume(WcRequestError.UnknownError(it.throwable).left()) }</ID>
<ID>MultilineLambdaItParameter:DefaultWcRespondService.kt$DefaultWcRespondService${ if (continuation.isCompleted) return@respondSessionRequest val result = when (val response = it.jsonRpcResponse) { is Wallet.Model.JsonRpcResponse.JsonRpcError -&gt; { Timber.tag(WC_TAG).e("Failed respond $response for request $request") WcRequestError.WcRespondError( code = response.code, message = response.message, ).left() } is Wallet.Model.JsonRpcResponse.JsonRpcResult -&gt; { Timber.tag(WC_TAG).i("Successful respond $response for request $request") if (response.result == null) { Timber.tag(WC_TAG).e( "Response result is null, but it should be String. Casted to empty", ) } (response.result ?: "").right() } } continuation.resume(result) }</ID>
<ID>MultilineLambdaItParameter:WcEthMessageSignUseCase.kt$LegacySdkHelper${ if (!it.isAscii()) return null Integer.toHexString(it.code) }</ID>
<ID>MultilineLambdaItParameter:WcEthMessageSignUseCase.kt$LegacySdkHelper${ val char = it.toInt().toChar() if (char.isAscii()) char else return null }</ID>
<ID>MultilineLambdaItParameter:WcEthNetwork.kt$WcEthNetwork${ if (this == WcEthMethodName.AddEthereumChain) { WcEthMethod.AddEthereumChain(rawChain = it).right() } else { WcEthMethod.SwitchEthereumChain(rawChain = it).right() } }</ID>
@ -31,9 +19,6 @@
<ID>MultilineLambdaItParameter:WcPairSdkDelegate.kt$WcPairSdkDelegate${ store.removePendingApproval(forSave) it.left() }</ID>
<ID>MultilineLambdaItParameter:WcSdkSessionConverter.kt$WcSdkSessionConverter${ WcAppMetaDataConverter.convert( value = WcAppMetaDataConverter.Input( originUrl = value.originUrl, peerMetaData = it, ), ) }</ID>
<ID>MultilineLambdaItParameter:WcSdkSessionRequestConverter.kt$WcSdkSessionRequestConverter${ WcAppMetaDataConverter.convert( value = WcAppMetaDataConverter.Input( originUrl = value.originUrl, peerMetaData = it, ), ) }</ID>
<ID>MultilineLambdaItParameter:WcSolanaSignTransactionUseCase.kt$WcSolanaSignTransactionUseCase${ analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Failed)) Timber.e(it.toString()) emit(state.toResult(parseSendError(it).left())) }</ID>
<ID>NamedArguments:AssociateNetworksDelegate.kt$AssociateNetworksDelegate$mapNetworksForPortfolio(wallet, null, requiredNamespaces, optionalNamespaces, sessionProposal)</ID>
<ID>NamedArguments:WcEthTxHelper.kt$WcEthTxHelper$ethSpecificFee(userWallet, currency, gasLimit, gasPrice)</ID>
<ID>NoNameShadowing:CaipNamespaceDelegate.kt$CaipNamespaceDelegate$optionalNamespaces</ID>
<ID>NoNameShadowing:CaipNamespaceDelegate.kt$CaipNamespaceDelegate$requiredNamespaces</ID>
<ID>NoNameShadowing:DefaultWcRespondService.kt$DefaultWcRespondService$response</ID>
@ -41,7 +26,6 @@
<ID>NullCheckOnMutableProperty:WcEthSendTransactionUseCase.kt$WcEthSendTransactionUseCase$if (dAppFee != null) return dAppFee</ID>
<ID>NullCheckOnMutableProperty:WcEthSignTransactionUseCase.kt$WcEthSignTransactionUseCase$if (dAppFee != null) return dAppFee</ID>
<ID>NullableBooleanCheck:DefaultWcPairService.kt$DefaultWcPairService$existSessionTopic(request.uri).getOrNull() ?: false</ID>
<ID>NullableToStringCall:WcSolanaSignTransactionUseCase.kt$WcSolanaSignTransactionUseCase$${e.message}</ID>
<ID>SuspendFunSwallowedCancellation:DefaultWcPairService.kt$DefaultWcPairService$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:DefaultWcPairUseCase.kt$DefaultWcPairUseCase$runCatching</ID>
<ID>UseAnyOrNoneInsteadOfFind:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$find { it.sdkModel.topic == dto.topic }</ID>

View file

@ -19,7 +19,9 @@ import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.walletconnect.WcTransactionSignerProvider
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcBitcoinMethod
import com.tangem.domain.walletconnect.model.WcPsbtOutput
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
import com.tangem.domain.walletconnect.usecase.method.WcPsbtUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -51,7 +53,8 @@ internal class WcBitcoinSignPsbtUseCase @AssistedInject constructor(
blockAidDelegate: BlockAidVerificationDelegate,
@SdkMoshi private val moshi: Moshi,
) : BaseWcSignUseCase<Nothing, TransactionData>(),
WcTransactionUseCase {
WcTransactionUseCase,
WcPsbtUseCase {
override val wallet get() = context.session.wallet
@ -120,6 +123,20 @@ internal class WcBitcoinSignPsbtUseCase @AssistedInject constructor(
}
}
override suspend fun parsePsbtOutputs(): List<WcPsbtOutput> {
val walletManager = walletManagersFacade.getOrCreateWalletManager(wallet.walletId, network)
?: return emptyList()
return when (val result = walletManager.parsePsbtOutputs(method.psbt)) {
is SdkResult.Success -> result.data.map { output ->
WcPsbtOutput(
address = output.address,
amountSatoshi = output.amountSatoshi,
)
}
is SdkResult.Failure -> emptyList()
}
}
override fun invoke(): Flow<WcSignState<TransactionData>> {
val transactionData = TransactionData.Compiled(
value = TransactionData.Compiled.Data.RawString(method.psbt),

View file

@ -2,6 +2,7 @@ package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchain.extensions.encodeBase58
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -13,6 +14,7 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.error.parseTangemSdkError
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
@ -22,6 +24,10 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
private val TransactionSignAttemptException = IllegalStateException(
"solana_signMessage payload is a serialized Solana transaction; signing rejected ([REDACTED_TASK_KEY])",
)
internal class WcSolanaMessageSignUseCase @AssistedInject constructor(
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignMessage,
@ -38,6 +44,15 @@ internal class WcSolanaMessageSignUseCase @AssistedInject constructor(
state: WcSignState<WcMessageSignUseCase.SignModel>,
) {
val hashToSign = method.rawMessage.decodeBase58() ?: byteArrayOf()
// Never blind-sign a payload that is actually a serialized Solana transaction message — the
// resulting signature would be a valid transaction signature that a malicious dApp could broadcast to move
// the user's funds. Reject such requests instead of signing them.
if (SolanaTransactionHelper.isTransactionMessage(hashToSign)) {
emit(state.toResult(WcRequestError.UnknownError(TransactionSignAttemptException).left()))
return
}
val userWallet = session.wallet
val signedHash = signUseCase(hashToSign, userWallet, network)

View file

@ -54,13 +54,17 @@ internal class BlockAidVerificationDelegate @Inject constructor(
is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction)
is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction))
is WcSolanaMethod.SignAndSendTransaction -> TransactionParams.Solana(listOf(method.transaction))
is WcSolanaMethod.SignMessage,
is WcBitcoinMethod,
-> {
// BlockAid doesn't support Solana message signing and Bitcoin methods
is WcSolanaMethod.SignMessage -> {
// BlockAid doesn't support Solana message signing
emit(Lce.Content(createSafeResult()))
return@flow
}
is WcBitcoinMethod -> {
// BlockAid doesn't support Bitcoin methods: don't synthesize a SAFE result for an unscanned
// transaction
emit(Lce.Content(failedResult))
return@flow
}
else -> {
emit(Lce.Content(failedResult))
return@flow

View file

@ -0,0 +1,144 @@
package com.tangem.data.walletconnect.network.solana
import app.cash.turbine.test
import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.blockchain.extensions.encodeBase58
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.models.account.Account
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class WcSolanaMessageSignUseCaseTest {
private val signUseCase: SignUseCase = mockk()
private val respondService: WcRespondService = mockk()
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
private val context: WcMethodUseCaseContext = WcMethodUseCaseContext(
network = MockCryptoCurrencyFactory().ethereum.network,
accountAddress = "",
rawSdkRequest = WcSdkSessionRequest(
topic = "",
chainId = "",
dAppMetaData = WcAppMetaData(name = "", description = "", url = "", icons = listOf(), redirect = ""),
request = WcSdkSessionRequest.JSONRPCRequest(id = 0L, method = "", params = ""),
),
networkDerivationsCount = 1,
session = WcSession(
wallet = MockUserWalletFactory.create(),
networks = setOf(),
account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId),
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
connectingTime = 0L,
sdkModel = WcSdkSession(
topic = "",
namespaces = mapOf(),
appMetaData = WcAppMetaData(name = "", description = "", url = "", icons = listOf(), redirect = ""),
),
showWalletInfo = false,
),
)
@BeforeEach
fun setup() {
clearMocks(signUseCase, respondService)
}
@Test
fun `GIVEN payload is a serialized transaction WHEN sign THEN request rejected without signing`() =
runTest(UnconfinedTestDispatcher()) {
// Arrange
val useCase = createUseCase(rawMessage = LEGACY_TRANSACTION_MESSAGE.encodeBase58())
// Act
useCase.invoke().test {
awaitItem() // initial PreSign state
useCase.sign()
val result = (expectMostRecentItem().domainStep as WcSignStep.Result).result
// Assert
assertTrue(result.isLeft())
assertTrue(result.leftOrNull() is WcRequestError.UnknownError)
}
coVerify(exactly = 0) {
signUseCase(
hash = any(),
userWallet = any(),
network = any(),
)
}
coVerify(exactly = 0) { respondService.respond(any(), any()) }
}
@Test
fun `GIVEN human-readable message WHEN sign THEN it is signed and responded`() =
runTest(UnconfinedTestDispatcher()) {
// Arrange
val message = "Sign in to Tangem\nNonce: 8f3a91c0d4".toByteArray()
coEvery {
signUseCase(
hash = any(),
userWallet = any(),
network = any(),
)
} returns byteArrayOf(0x0A, 0x0B, 0x0C).right()
coEvery { respondService.respond(any(), any()) } returns RESPOND_RESULT.right()
val useCase = createUseCase(rawMessage = message.encodeBase58())
// Act
useCase.invoke().test {
awaitItem() // initial PreSign state
useCase.sign()
val result = (expectMostRecentItem().domainStep as WcSignStep.Result).result
// Assert
assertEquals(RESPOND_RESULT.right(), result)
}
coVerify(exactly = 1) { signUseCase(any(), context.session.wallet, context.network) }
coVerify(exactly = 1) { respondService.respond(any(), any()) }
}
private fun createUseCase(rawMessage: String) = WcSolanaMessageSignUseCase(
context = context,
method = WcSolanaMethod.SignMessage(pubKey = "", rawMessage = rawMessage, humanMsg = ""),
signUseCase = signUseCase,
respondService = respondService,
analytics = analytics,
)
private companion object {
const val RESPOND_RESULT = "{ signature: \"signature\" }"
// A minimal but well-formed legacy Solana message: header + 2 accounts + blockhash + 1 instruction.
val LEGACY_TRANSACTION_MESSAGE: ByteArray = byteArrayOf(0x01, 0x00, 0x01) + // message header
byteArrayOf(0x02) + ByteArray(size = 2 * 32) + // 2 account keys
ByteArray(size = 32) + // recent blockhash
byteArrayOf(0x01) + // instruction count
byteArrayOf(0x01) + // program id index
byteArrayOf(0x01, 0x00) + // 1 account index = [0]
byteArrayOf(0x03, 0x0A, 0x0B, 0x0C) // data length 3 + 3 data bytes
}
}

View file

@ -25,10 +25,10 @@ import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import org.junit.jupiter.api.Assertions.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
internal class DefaultWcPairUseCaseTest {
@ -119,7 +119,7 @@ internal class DefaultWcPairUseCaseTest {
pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source),
)
@Before
@BeforeEach
fun setup() {
coEvery { associateNetworksDelegate.associateAccounts(sdkProposal) } returns mapOf()
coEvery {

View file

@ -19,12 +19,12 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import org.junit.jupiter.api.Assertions.assertEquals
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
internal class WcSignUseCaseDelegateTest {
@ -107,7 +107,7 @@ internal class WcSignUseCaseDelegateTest {
middleActionCollector = middleActionCollector,
)
@Before
@BeforeEach
fun setup() {
middleActionCollector = object : MiddleActionCollector<TestMiddleAction, TestSignModel> {}
finalActionCollector = object : FinalActionCollector<TestSignModel> {}