Updated on 2026-08-14
This commit is contained in:
parent
254b5cd08d
commit
c540257c66
3 changed files with 149 additions and 2 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
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(any(), any(), 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(any(), any(), 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue