Updated on 2026-08-14
This commit is contained in:
parent
5ac7254d82
commit
e795c935aa
15 changed files with 270 additions and 242 deletions
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.addressbook.usecase.ValidateContactAddressUseCase
|
||||||
import com.tangem.domain.addressbook.usecase.VerifyAddressEntriesUseCase
|
import com.tangem.domain.addressbook.usecase.VerifyAddressEntriesUseCase
|
||||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||||
import com.tangem.domain.transaction.usecase.VerifyMessagesUseCase
|
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -29,7 +29,9 @@ object AddressBookDomainModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideVerifyAddressEntriesUseCase(verifyMessagesUseCase: VerifyMessagesUseCase): VerifyAddressEntriesUseCase {
|
fun provideVerifyAddressEntriesUseCase(
|
||||||
|
verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||||
|
): VerifyAddressEntriesUseCase {
|
||||||
return VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
return VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -237,20 +237,8 @@ internal object TransactionDomainModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideSignHashesUseCase(
|
fun provideVerifySecp256k1MessagesUseCase(): VerifySecp256k1MessagesUseCase {
|
||||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
return VerifySecp256k1MessagesUseCase()
|
||||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
|
||||||
): SignHashesUseCase {
|
|
||||||
return SignHashesUseCase(
|
|
||||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
|
||||||
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideVerifyMessagesUseCase(): VerifyMessagesUseCase {
|
|
||||||
return VerifyMessagesUseCase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,13 @@ internal class WcSolanaMessageSignUseCaseTest {
|
||||||
assertTrue(result.isLeft())
|
assertTrue(result.isLeft())
|
||||||
assertTrue(result.leftOrNull() is WcRequestError.UnknownError)
|
assertTrue(result.leftOrNull() is WcRequestError.UnknownError)
|
||||||
}
|
}
|
||||||
coVerify(exactly = 0) { signUseCase(any(), any(), any()) }
|
coVerify(exactly = 0) {
|
||||||
|
signUseCase(
|
||||||
|
hash = any(),
|
||||||
|
userWallet = any(),
|
||||||
|
network = any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
coVerify(exactly = 0) { respondService.respond(any(), any()) }
|
coVerify(exactly = 0) { respondService.respond(any(), any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +98,13 @@ internal class WcSolanaMessageSignUseCaseTest {
|
||||||
runTest(UnconfinedTestDispatcher()) {
|
runTest(UnconfinedTestDispatcher()) {
|
||||||
// Arrange
|
// Arrange
|
||||||
val message = "Sign in to Tangem\nNonce: 8f3a91c0d4".toByteArray()
|
val message = "Sign in to Tangem\nNonce: 8f3a91c0d4".toByteArray()
|
||||||
coEvery { signUseCase(any(), any(), any()) } returns byteArrayOf(0x0A, 0x0B, 0x0C).right()
|
coEvery {
|
||||||
|
signUseCase(
|
||||||
|
hash = any(),
|
||||||
|
userWallet = any(),
|
||||||
|
network = any(),
|
||||||
|
)
|
||||||
|
} returns byteArrayOf(0x0A, 0x0B, 0x0C).right()
|
||||||
coEvery { respondService.respond(any(), any()) } returns RESPOND_RESULT.right()
|
coEvery { respondService.respond(any(), any()) } returns RESPOND_RESULT.right()
|
||||||
val useCase = createUseCase(rawMessage = message.encodeBase58())
|
val useCase = createUseCase(rawMessage = message.encodeBase58())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,6 @@ dependencies {
|
||||||
// region Test libraries
|
// region Test libraries
|
||||||
testImplementation(projects.test.core)
|
testImplementation(projects.test.core)
|
||||||
testImplementation(projects.test.mock)
|
testImplementation(projects.test.mock)
|
||||||
|
testImplementation(projects.common.test)
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
@ -6,25 +6,27 @@ import com.tangem.domain.addressbook.model.AddressEntry
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.transaction.error.SignHashesError
|
import com.tangem.domain.transaction.error.SignHashesError
|
||||||
import com.tangem.domain.transaction.usecase.SignHashesUseCase
|
import com.tangem.domain.transaction.usecase.SignUseCase
|
||||||
|
import com.tangem.domain.transaction.usecase.primarySecp256k1PublicKey
|
||||||
import com.tangem.utils.extensions.toHexString
|
import com.tangem.utils.extensions.toHexString
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signs every [AddressEntry] of a [Contact] with the wallet's primary key in a single signing
|
* Signs every [AddressEntry] of a [Contact] with the wallet's primary secp256k1 key in a single
|
||||||
* session (one card tap). Each entry is hashed as `SHA-256(address + networkId + memo + contactId +
|
* signing session (one card tap). Each entry is hashed as `SHA-256(address + networkId + memo +
|
||||||
* name)` and the produced signature is stored back into [AddressEntry.signature].
|
* contactId + name)` and the produced signature is stored back into [AddressEntry.signature].
|
||||||
*/
|
*/
|
||||||
class SignAddressEntriesUseCase(
|
class SignAddressEntriesUseCase(
|
||||||
private val signHashesUseCase: SignHashesUseCase,
|
private val signUseCase: SignUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(userWallet: UserWallet, contact: Contact): Either<SignHashesError, Contact> = either {
|
suspend operator fun invoke(userWallet: UserWallet, contact: Contact): Either<SignHashesError, Contact> = either {
|
||||||
val entries = contact.addressEntries
|
val entries = contact.addressEntries
|
||||||
if (entries.isEmpty()) return@either contact
|
if (entries.isEmpty()) return@either contact
|
||||||
|
|
||||||
|
val publicKey = userWallet.primarySecp256k1PublicKey() ?: raise(SignHashesError.NoSigningKey)
|
||||||
val hashes = entries.map { entry -> hashEntry(contact, entry) }
|
val hashes = entries.map { entry -> hashEntry(contact, entry) }
|
||||||
val signatures = signHashesUseCase(userWallet = userWallet, hashes = hashes).bind()
|
val signatures = signUseCase(hashes = hashes, publicKey = publicKey, userWallet = userWallet).bind()
|
||||||
|
|
||||||
val signedEntries = entries.mapIndexed { index, entry ->
|
val signedEntries = entries.mapIndexed { index, entry ->
|
||||||
entry.copy(signature = signatures[index].toHexString())
|
entry.copy(signature = signatures[index].toHexString())
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import com.tangem.domain.addressbook.model.AddressEntry
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.transaction.error.VerifyMessagesError
|
import com.tangem.domain.transaction.error.VerifyMessagesError
|
||||||
import com.tangem.domain.transaction.usecase.VerifyMessagesUseCase
|
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
|
||||||
import com.tangem.utils.extensions.hexToBytesOrNull
|
import com.tangem.utils.extensions.hexToBytesOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,7 +23,7 @@ import com.tangem.utils.extensions.hexToBytesOrNull
|
||||||
* Each entry is verified against the exact bytes that were signed (see [buildAddressEntryPayload]).
|
* Each entry is verified against the exact bytes that were signed (see [buildAddressEntryPayload]).
|
||||||
*/
|
*/
|
||||||
class VerifyAddressEntriesUseCase(
|
class VerifyAddressEntriesUseCase(
|
||||||
private val verifyMessagesUseCase: VerifyMessagesUseCase,
|
private val verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.usecase
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||||
import com.tangem.domain.addressbook.model.AddressEntry
|
import com.tangem.domain.addressbook.model.AddressEntry
|
||||||
import com.tangem.domain.addressbook.model.AddressEntryId
|
import com.tangem.domain.addressbook.model.AddressEntryId
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
|
|
@ -12,11 +13,12 @@ import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.transaction.error.SignHashesError
|
import com.tangem.domain.transaction.error.SignHashesError
|
||||||
import com.tangem.domain.transaction.usecase.SignHashesUseCase
|
import com.tangem.domain.transaction.usecase.SignUseCase
|
||||||
import com.tangem.utils.extensions.toHexString
|
import com.tangem.utils.extensions.toHexString
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -28,14 +30,16 @@ import java.security.MessageDigest
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
class SignAddressEntriesUseCaseTest {
|
class SignAddressEntriesUseCaseTest {
|
||||||
|
|
||||||
private val signHashesUseCase: SignHashesUseCase = mockk()
|
private val signUseCase: SignUseCase = mockk()
|
||||||
private val useCase = SignAddressEntriesUseCase(signHashesUseCase = signHashesUseCase)
|
private val useCase = SignAddressEntriesUseCase(signUseCase = signUseCase)
|
||||||
|
|
||||||
private val userWallet: UserWallet = mockk()
|
// The mock factory builds each wallet key with publicKey = curve.name bytes, so the secp256k1 key is "Secp256k1"
|
||||||
|
private val userWallet: UserWallet = MockUserWalletFactory.create()
|
||||||
|
private val secp256k1Key = "Secp256k1".toByteArray()
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun resetMocks() {
|
fun resetMocks() {
|
||||||
clearMocks(signHashesUseCase)
|
clearMocks(signUseCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -47,7 +51,10 @@ class SignAddressEntriesUseCaseTest {
|
||||||
)
|
)
|
||||||
val signatures = listOf(byteArrayOf(0x01, 0xAB.toByte()), byteArrayOf(0xCD.toByte()))
|
val signatures = listOf(byteArrayOf(0x01, 0xAB.toByte()), byteArrayOf(0xCD.toByte()))
|
||||||
val hashesSlot = slot<List<ByteArray>>()
|
val hashesSlot = slot<List<ByteArray>>()
|
||||||
coEvery { signHashesUseCase(eq(userWallet), capture(hashesSlot)) } returns signatures.right()
|
val publicKeySlot = slot<ByteArray>()
|
||||||
|
coEvery {
|
||||||
|
signUseCase(hashes = capture(hashesSlot), publicKey = capture(publicKeySlot), userWallet = eq(userWallet))
|
||||||
|
} returns signatures.right()
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
val result = useCase(userWallet, contact)
|
val result = useCase(userWallet, contact)
|
||||||
|
|
@ -61,6 +68,8 @@ class SignAddressEntriesUseCaseTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertThat(result.getOrNull()).isEqualTo(expected)
|
assertThat(result.getOrNull()).isEqualTo(expected)
|
||||||
|
// The wallet's primary secp256k1 key is the one signing
|
||||||
|
assertThat(publicKeySlot.captured).isEqualTo(secp256k1Key)
|
||||||
// Each entry is hashed as SHA-256(address + networkId + memo + contactId + name), in order
|
// Each entry is hashed as SHA-256(address + networkId + memo + contactId + name), in order
|
||||||
assertThat(hashesSlot.captured.map { it.toHexString() })
|
assertThat(hashesSlot.captured.map { it.toHexString() })
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
|
|
@ -80,20 +89,35 @@ class SignAddressEntriesUseCaseTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result.getOrNull()).isEqualTo(contact)
|
assertThat(result.getOrNull()).isEqualTo(contact)
|
||||||
coVerify(exactly = 0) { signHashesUseCase(any(), any()) }
|
coVerify(exactly = 0) { signUseCase(any<List<ByteArray>>(), any(), any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN signHashesUseCase returns error WHEN invoke THEN propagates the error`() = runTest {
|
fun `GIVEN wallet without a secp256k1 key WHEN invoke THEN returns NoSigningKey without signing`() = runTest {
|
||||||
|
// Arrange — a locked hot wallet exposes no key
|
||||||
|
val lockedWallet = mockk<UserWallet.Hot> { every { wallets } returns null }
|
||||||
|
val contact = contact(entry(id = "addr-1", address = "0xabc", memo = null))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(lockedWallet, contact)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.NoSigningKey)
|
||||||
|
coVerify(exactly = 0) { signUseCase(any<List<ByteArray>>(), any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN signUseCase returns error WHEN invoke THEN propagates the error`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val contact = contact(entry(id = "addr-1", address = "0xabc", memo = null))
|
val contact = contact(entry(id = "addr-1", address = "0xabc", memo = null))
|
||||||
coEvery { signHashesUseCase(any(), any()) } returns SignHashesError.NoSigningKey.left()
|
coEvery { signUseCase(any<List<ByteArray>>(), any(), any()) } returns
|
||||||
|
SignHashesError.SigningFailed(message = "canceled").left()
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
val result = useCase(userWallet, contact)
|
val result = useCase(userWallet, contact)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.NoSigningKey)
|
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.SigningFailed(message = "canceled"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun contact(vararg entries: AddressEntry): Contact = Contact(
|
private fun contact(vararg entries: AddressEntry): Contact = Contact(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.transaction.error.VerifyMessagesError
|
import com.tangem.domain.transaction.error.VerifyMessagesError
|
||||||
import com.tangem.domain.transaction.usecase.VerifyMessagesUseCase
|
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
|
||||||
import com.tangem.utils.extensions.toHexString
|
import com.tangem.utils.extensions.toHexString
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -26,7 +26,7 @@ import org.junit.jupiter.api.TestInstance
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
class VerifyAddressEntriesUseCaseTest {
|
class VerifyAddressEntriesUseCaseTest {
|
||||||
|
|
||||||
private val verifyMessagesUseCase: VerifyMessagesUseCase = mockk()
|
private val verifyMessagesUseCase: VerifySecp256k1MessagesUseCase = mockk()
|
||||||
private val useCase = VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
private val useCase = VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
||||||
|
|
||||||
private val userWallet: UserWallet = mockk()
|
private val userWallet: UserWallet = mockk()
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@ import com.tangem.domain.models.wallet.UserWallet
|
||||||
* Wallet master secp256k1 public key bytes, without any network derivation. Returns `null` when the
|
* Wallet master secp256k1 public key bytes, without any network derivation. Returns `null` when the
|
||||||
* wallet is locked or has no secp256k1 key.
|
* wallet is locked or has no secp256k1 key.
|
||||||
*
|
*
|
||||||
* This is the single source of truth for the key used to sign ([SignHashesUseCase]) and verify
|
* This is the single source of truth for the key a caller hands to [SignUseCase] and the key
|
||||||
* ([VerifyMessagesUseCase]) raw hashes, so both operations resolve to the very same key.
|
* [VerifySecp256k1MessagesUseCase] verifies against, so both operations resolve to the very same key.
|
||||||
|
* The curve stays encapsulated here (callers receive plain bytes) so they don't depend on the card SDK.
|
||||||
*/
|
*/
|
||||||
internal fun UserWallet.primarySecp256k1PublicKey(): ByteArray? = when (this) {
|
fun UserWallet.primarySecp256k1PublicKey(): ByteArray? = when (this) {
|
||||||
is UserWallet.Cold -> scanResponse.card.wallets
|
is UserWallet.Cold -> scanResponse.card.wallets
|
||||||
.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||||
?.publicKey
|
?.publicKey
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package com.tangem.domain.transaction.usecase
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import arrow.core.left
|
|
||||||
import arrow.core.right
|
|
||||||
import com.tangem.blockchain.common.TransactionSigner
|
|
||||||
import com.tangem.blockchain.common.Wallet
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
|
||||||
import com.tangem.domain.card.models.TwinKey
|
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.domain.transaction.error.SignHashesError
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs a batch of raw [hashes] with the wallet's primary secp256k1 key in a single signing
|
|
||||||
* session — one NFC tap for cold cards, one access-code unlock for hot wallets.
|
|
||||||
*
|
|
||||||
* The hashes are signed with the wallet master key without any network derivation, so every
|
|
||||||
* signature verifies against that single wallet public key regardless of which networks the hashed
|
|
||||||
* data refers to. Use it when several pieces of data must be attested with the same wallet identity
|
|
||||||
* in one user interaction (e.g. signing all address-book entries of a contact at once).
|
|
||||||
*
|
|
||||||
* Signatures are returned in the same order as the input [hashes].
|
|
||||||
*/
|
|
||||||
class SignHashesUseCase(
|
|
||||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
|
||||||
private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner,
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
userWallet: UserWallet,
|
|
||||||
hashes: List<ByteArray>,
|
|
||||||
): Either<SignHashesError, List<ByteArray>> {
|
|
||||||
if (hashes.isEmpty()) return emptyList<ByteArray>().right()
|
|
||||||
|
|
||||||
val seedKey = userWallet.primarySecp256k1PublicKey() ?: return SignHashesError.NoSigningKey.left()
|
|
||||||
val publicKey = Wallet.PublicKey(seedKey = seedKey, derivationType = null)
|
|
||||||
|
|
||||||
val signer = when (userWallet) {
|
|
||||||
is UserWallet.Hot -> getHotTransactionSigner(userWallet)
|
|
||||||
is UserWallet.Cold -> getColdSigner(userWallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
return when (val result = signer.sign(hashes, publicKey)) {
|
|
||||||
is CompletionResult.Success -> result.data.right()
|
|
||||||
is CompletionResult.Failure -> SignHashesError.SigningFailed(
|
|
||||||
message = result.error.message ?: "Unknown error",
|
|
||||||
).left()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner {
|
|
||||||
val card = userWallet.scanResponse.card
|
|
||||||
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
|
|
||||||
|
|
||||||
return cardSdkConfigRepository.getCommonSigner(
|
|
||||||
cardId = card.cardId.takeIf { isCardNotBackedUp },
|
|
||||||
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,12 +4,14 @@ import arrow.core.Either
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.tangem.blockchain.common.TransactionSigner
|
import com.tangem.blockchain.common.TransactionSigner
|
||||||
|
import com.tangem.blockchain.common.Wallet
|
||||||
import com.tangem.common.CompletionResult
|
import com.tangem.common.CompletionResult
|
||||||
import com.tangem.common.core.TangemError
|
import com.tangem.common.core.TangemError
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
||||||
import com.tangem.domain.card.models.TwinKey
|
import com.tangem.domain.card.models.TwinKey
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.transaction.error.SignHashesError
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
|
||||||
|
|
@ -37,6 +39,39 @@ class SignUseCase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs a batch of raw [hashes] with [publicKey] in a single signing session — one NFC tap for
|
||||||
|
* cold cards, one access-code unlock for hot wallets.
|
||||||
|
*
|
||||||
|
* [publicKey] is the wallet seed key the hashes are signed with, without any network derivation,
|
||||||
|
* so every signature verifies against that single public key regardless of which networks the
|
||||||
|
* hashed data refers to. Resolving which key to sign with (and its curve) is the caller's
|
||||||
|
* responsibility — this use case is curve-agnostic and signs exactly the given hashes.
|
||||||
|
*
|
||||||
|
* Signatures are returned in the same order as the input [hashes]; an empty input yields an empty
|
||||||
|
* list without starting a signing session.
|
||||||
|
*/
|
||||||
|
suspend operator fun invoke(
|
||||||
|
hashes: List<ByteArray>,
|
||||||
|
publicKey: ByteArray,
|
||||||
|
userWallet: UserWallet,
|
||||||
|
): Either<SignHashesError, List<ByteArray>> {
|
||||||
|
if (hashes.isEmpty()) return emptyList<ByteArray>().right()
|
||||||
|
|
||||||
|
val signer = when (userWallet) {
|
||||||
|
is UserWallet.Hot -> getHotTransactionSigner(userWallet)
|
||||||
|
is UserWallet.Cold -> getColdSigner(userWallet)
|
||||||
|
}
|
||||||
|
val seedPublicKey = Wallet.PublicKey(seedKey = publicKey, derivationType = null)
|
||||||
|
|
||||||
|
return when (val result = signer.sign(hashes, seedPublicKey)) {
|
||||||
|
is CompletionResult.Success -> result.data.right()
|
||||||
|
is CompletionResult.Failure -> SignHashesError.SigningFailed(
|
||||||
|
message = result.error.message ?: "Unknown error",
|
||||||
|
).left()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner {
|
private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner {
|
||||||
val card = userWallet.scanResponse.card
|
val card = userWallet.scanResponse.card
|
||||||
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
|
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,15 @@ import com.tangem.domain.transaction.error.VerifyMessagesError
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies each of the given [messages] against [userWallet]'s primary secp256k1 key — the
|
* Verifies each of the given [messages] against [userWallet]'s primary secp256k1 key — the
|
||||||
* counterpart of [SignHashesUseCase].
|
* counterpart of [SignUseCase] for raw-hash signatures.
|
||||||
*
|
*
|
||||||
* Pass the **original messages** (the pre-images), not their hashes: signing hashes a message with
|
* Only secp256k1 is supported: the verifier hashes the message with SHA-256 and runs ECDSA, which
|
||||||
* SHA-256 before the elliptic-curve operation, so verification applies the same SHA-256 internally
|
* mirrors how the card signs (`SHA-256(message)` then ECDSA). Other curves use a different hashing
|
||||||
* (via [CryptoUtils.verify]). [messages] and [signatures] are positional — element `i` of one must
|
* scheme, so this use case is deliberately curve-specific rather than parameterized.
|
||||||
* correspond to element `i` of the other.
|
*
|
||||||
|
* Pass the **original messages** (the pre-images), not their hashes: the SHA-256 is applied
|
||||||
|
* internally (via [CryptoUtils.verify]). [messages] and [signatures] are positional — element `i` of
|
||||||
|
* one must correspond to element `i` of the other.
|
||||||
*
|
*
|
||||||
* Returns one [Boolean] per message, aligned to [messages] order: `result[i]` is `true` only when
|
* Returns one [Boolean] per message, aligned to [messages] order: `result[i]` is `true` only when
|
||||||
* `signatures[i]` is a valid signature of `messages[i]`. A mismatch (tampered data, wrong wallet,
|
* `signatures[i]` is a valid signature of `messages[i]`. A mismatch (tampered data, wrong wallet,
|
||||||
|
|
@ -23,7 +26,7 @@ import com.tangem.domain.transaction.error.VerifyMessagesError
|
||||||
* wallet's signing key being unavailable is a [VerifyMessagesError.NoSigningKey] failure (nothing can
|
* wallet's signing key being unavailable is a [VerifyMessagesError.NoSigningKey] failure (nothing can
|
||||||
* be verified) rather than a list of `false`s.
|
* be verified) rather than a list of `false`s.
|
||||||
*/
|
*/
|
||||||
class VerifyMessagesUseCase {
|
class VerifySecp256k1MessagesUseCase {
|
||||||
|
|
||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
package com.tangem.domain.transaction.usecase
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import com.tangem.blockchain.common.TransactionSigner
|
|
||||||
import com.tangem.blockchain.common.Wallet
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.common.card.EllipticCurve
|
|
||||||
import com.tangem.common.core.TangemError
|
|
||||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|
||||||
import com.tangem.domain.models.MobileWallet
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.domain.transaction.error.SignHashesError
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.slot
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
internal class SignHashesUseCaseTest {
|
|
||||||
|
|
||||||
private val cardSdkConfigRepository: CardSdkConfigRepository = mockk()
|
|
||||||
private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner = mockk()
|
|
||||||
|
|
||||||
private val useCase = SignHashesUseCase(
|
|
||||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
|
||||||
getHotTransactionSigner = getHotTransactionSigner,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val hashes = listOf(byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6))
|
|
||||||
private val signatures = listOf(byteArrayOf(7, 8, 9), byteArrayOf(10, 11, 12))
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN cold wallet with secp256k1 key WHEN invoke THEN signs hashes with common signer`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
val coldWallet = MockUserWalletFactory.create()
|
|
||||||
val signer: TransactionSigner = mockk()
|
|
||||||
val publicKeySlot = slot<Wallet.PublicKey>()
|
|
||||||
|
|
||||||
every { cardSdkConfigRepository.getCommonSigner(any(), any()) } returns signer
|
|
||||||
coEvery { signer.sign(eq(hashes), capture(publicKeySlot)) } returns CompletionResult.Success(signatures)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = useCase(coldWallet, hashes)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result.getOrNull()).isEqualTo(signatures)
|
|
||||||
// Wallet master secp256k1 key is used, without network derivation
|
|
||||||
assertThat(publicKeySlot.captured.seedKey).isEqualTo(EllipticCurve.Secp256k1.name.toByteArray())
|
|
||||||
assertThat(publicKeySlot.captured.derivationType).isNull()
|
|
||||||
// Card is not backed up (backupStatus == null) and not a twin, so its id is passed to the signer
|
|
||||||
verify(exactly = 1) { cardSdkConfigRepository.getCommonSigner(cardId = coldWallet.cardId, twinKey = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN hot wallet with secp256k1 key WHEN invoke THEN signs hashes with hot signer`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
val hotWallet = mockk<UserWallet.Hot> {
|
|
||||||
every { wallets } returns listOf(mobileWallet(curve = EllipticCurve.Secp256k1, publicKey = byteArrayOf(42)))
|
|
||||||
}
|
|
||||||
val signer: TransactionSigner = mockk()
|
|
||||||
val publicKeySlot = slot<Wallet.PublicKey>()
|
|
||||||
|
|
||||||
every { getHotTransactionSigner(hotWallet) } returns signer
|
|
||||||
coEvery { signer.sign(eq(hashes), capture(publicKeySlot)) } returns CompletionResult.Success(signatures)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = useCase(hotWallet, hashes)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result.getOrNull()).isEqualTo(signatures)
|
|
||||||
assertThat(publicKeySlot.captured.seedKey).isEqualTo(byteArrayOf(42))
|
|
||||||
assertThat(publicKeySlot.captured.derivationType).isNull()
|
|
||||||
verify(exactly = 1) { getHotTransactionSigner(hotWallet) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN locked wallet without signing key WHEN invoke THEN returns NoSigningKey`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
val lockedWallet = mockk<UserWallet.Hot> {
|
|
||||||
every { wallets } returns null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = useCase(lockedWallet, hashes)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.NoSigningKey)
|
|
||||||
verify(exactly = 0) { getHotTransactionSigner(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN signer fails WHEN invoke THEN returns SigningFailed with error message`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
val coldWallet = MockUserWalletFactory.create()
|
|
||||||
val signer: TransactionSigner = mockk()
|
|
||||||
val error: TangemError = mockk { every { message } returns "Signing canceled" }
|
|
||||||
|
|
||||||
every { cardSdkConfigRepository.getCommonSigner(any(), any()) } returns signer
|
|
||||||
coEvery { signer.sign(any<List<ByteArray>>(), any()) } returns CompletionResult.Failure(error)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = useCase(coldWallet, hashes)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.SigningFailed(message = "Signing canceled"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN empty hashes WHEN invoke THEN returns empty list without signing`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
val coldWallet = MockUserWalletFactory.create()
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = useCase(coldWallet, hashes = emptyList())
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result.getOrNull()).isEmpty()
|
|
||||||
verify(exactly = 0) { cardSdkConfigRepository.getCommonSigner(any(), any()) }
|
|
||||||
verify(exactly = 0) { getHotTransactionSigner(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mobileWallet(curve: EllipticCurve, publicKey: ByteArray): MobileWallet = MobileWallet(
|
|
||||||
publicKey = publicKey,
|
|
||||||
chainCode = null,
|
|
||||||
curve = curve,
|
|
||||||
derivedKeys = emptyMap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package com.tangem.domain.transaction.usecase
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.blockchain.common.TransactionSigner
|
||||||
|
import com.tangem.blockchain.common.Wallet
|
||||||
|
import com.tangem.blockchain.common.WalletManager
|
||||||
|
import com.tangem.common.CompletionResult
|
||||||
|
import com.tangem.common.core.TangemError
|
||||||
|
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||||
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.transaction.error.SignHashesError
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class SignUseCaseTest {
|
||||||
|
|
||||||
|
private val cardSdkConfigRepository: CardSdkConfigRepository = mockk()
|
||||||
|
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||||
|
private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner = mockk()
|
||||||
|
|
||||||
|
private val useCase = SignUseCase(
|
||||||
|
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
getHotTransactionSigner = getHotTransactionSigner,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val publicKey = byteArrayOf(42, 43, 44)
|
||||||
|
private val hashes = listOf(byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6))
|
||||||
|
private val signatures = listOf(byteArrayOf(7, 8, 9), byteArrayOf(10, 11, 12))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN cold wallet WHEN sign single hash THEN signs with the wallet-manager key for the network`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val coldWallet = MockUserWalletFactory.create()
|
||||||
|
val network: Network = mockk()
|
||||||
|
val signer: TransactionSigner = mockk()
|
||||||
|
val walletManagerKey = Wallet.PublicKey(seedKey = byteArrayOf(50, 51), derivationType = null)
|
||||||
|
val walletManager: WalletManager = mockk { every { wallet } returns mockk { every { publicKey } returns walletManagerKey } }
|
||||||
|
val hash = byteArrayOf(1, 2, 3)
|
||||||
|
val signature = byteArrayOf(9, 9)
|
||||||
|
|
||||||
|
every { cardSdkConfigRepository.getCommonSigner(any(), any()) } returns signer
|
||||||
|
coEvery { walletManagersFacade.getOrCreateWalletManager(coldWallet.walletId, network) } returns walletManager
|
||||||
|
coEvery { signer.sign(eq(hash), eq(walletManagerKey)) } returns CompletionResult.Success(signature)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(hash = hash, userWallet = coldWallet, network = network)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.getOrNull()).isEqualTo(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN signer fails WHEN sign single hash THEN returns the TangemError`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val coldWallet = MockUserWalletFactory.create()
|
||||||
|
val network: Network = mockk()
|
||||||
|
val signer: TransactionSigner = mockk()
|
||||||
|
val walletManagerKey = Wallet.PublicKey(seedKey = byteArrayOf(50, 51), derivationType = null)
|
||||||
|
val walletManager: WalletManager = mockk { every { wallet } returns mockk { every { publicKey } returns walletManagerKey } }
|
||||||
|
val error: TangemError = mockk()
|
||||||
|
|
||||||
|
every { cardSdkConfigRepository.getCommonSigner(any(), any()) } returns signer
|
||||||
|
coEvery { walletManagersFacade.getOrCreateWalletManager(coldWallet.walletId, network) } returns walletManager
|
||||||
|
coEvery { signer.sign(any<ByteArray>(), any()) } returns CompletionResult.Failure(error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(hash = byteArrayOf(1), userWallet = coldWallet, network = network)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.leftOrNull()).isEqualTo(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN cold wallet WHEN sign hashes THEN signs with common signer and given key`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val coldWallet = MockUserWalletFactory.create()
|
||||||
|
val signer: TransactionSigner = mockk()
|
||||||
|
val publicKeySlot = slot<Wallet.PublicKey>()
|
||||||
|
|
||||||
|
every { cardSdkConfigRepository.getCommonSigner(any(), any()) } returns signer
|
||||||
|
coEvery { signer.sign(eq(hashes), capture(publicKeySlot)) } returns CompletionResult.Success(signatures)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(hashes = hashes, publicKey = publicKey, userWallet = coldWallet)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.getOrNull()).isEqualTo(signatures)
|
||||||
|
// The caller-provided key is used verbatim, with no network derivation
|
||||||
|
assertThat(publicKeySlot.captured.seedKey).isEqualTo(publicKey)
|
||||||
|
assertThat(publicKeySlot.captured.derivationType).isNull()
|
||||||
|
// Card is not backed up (backupStatus == null) and not a twin, so its id is passed to the signer
|
||||||
|
verify(exactly = 1) { cardSdkConfigRepository.getCommonSigner(cardId = coldWallet.cardId, twinKey = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN hot wallet WHEN sign hashes THEN signs with hot signer and given key`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val hotWallet = mockk<UserWallet.Hot>()
|
||||||
|
val signer: TransactionSigner = mockk()
|
||||||
|
val publicKeySlot = slot<Wallet.PublicKey>()
|
||||||
|
|
||||||
|
every { getHotTransactionSigner(hotWallet) } returns signer
|
||||||
|
coEvery { signer.sign(eq(hashes), capture(publicKeySlot)) } returns CompletionResult.Success(signatures)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(hashes = hashes, publicKey = publicKey, userWallet = hotWallet)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.getOrNull()).isEqualTo(signatures)
|
||||||
|
assertThat(publicKeySlot.captured.seedKey).isEqualTo(publicKey)
|
||||||
|
verify(exactly = 1) { getHotTransactionSigner(hotWallet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN signer fails WHEN sign hashes THEN returns SigningFailed with error message`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val coldWallet = MockUserWalletFactory.create()
|
||||||
|
val signer: TransactionSigner = mockk()
|
||||||
|
val error: TangemError = mockk { every { message } returns "Signing canceled" }
|
||||||
|
|
||||||
|
every { cardSdkConfigRepository.getCommonSigner(any(), any()) } returns signer
|
||||||
|
coEvery { signer.sign(any<List<ByteArray>>(), any()) } returns CompletionResult.Failure(error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(hashes = hashes, publicKey = publicKey, userWallet = coldWallet)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.SigningFailed(message = "Signing canceled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN empty hashes WHEN sign hashes THEN returns empty list without signing`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val coldWallet = MockUserWalletFactory.create()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(hashes = emptyList(), publicKey = publicKey, userWallet = coldWallet)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.getOrNull()).isEmpty()
|
||||||
|
verify(exactly = 0) { cardSdkConfigRepository.getCommonSigner(any(), any()) }
|
||||||
|
verify(exactly = 0) { getHotTransactionSigner(any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,9 +16,9 @@ import org.junit.jupiter.api.TestInstance
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
internal class VerifyMessagesUseCaseTest {
|
internal class VerifySecp256k1MessagesUseCaseTest {
|
||||||
|
|
||||||
private val useCase = VerifyMessagesUseCase()
|
private val useCase = VerifySecp256k1MessagesUseCase()
|
||||||
|
|
||||||
// A valid secp256k1 key pair. The card signs the raw SHA-256 digest of each message.
|
// A valid secp256k1 key pair. The card signs the raw SHA-256 digest of each message.
|
||||||
private val privateKey = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632550".hexToBytes()
|
private val privateKey = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632550".hexToBytes()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue