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

@ -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> {}