Updated on 2026-08-14
This commit is contained in:
parent
5ac7254d82
commit
e795c935aa
15 changed files with 270 additions and 242 deletions
|
|
@ -23,5 +23,6 @@ dependencies {
|
|||
// region Test libraries
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.test.mock)
|
||||
testImplementation(projects.common.test)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -6,25 +6,27 @@ import com.tangem.domain.addressbook.model.AddressEntry
|
|||
import com.tangem.domain.addressbook.model.Contact
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
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 java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Signs every [AddressEntry] of a [Contact] with the wallet's primary key in a single signing
|
||||
* session (one card tap). Each entry is hashed as `SHA-256(address + networkId + memo + contactId +
|
||||
* name)` and the produced signature is stored back into [AddressEntry.signature].
|
||||
* Signs every [AddressEntry] of a [Contact] with the wallet's primary secp256k1 key in a single
|
||||
* signing session (one card tap). Each entry is hashed as `SHA-256(address + networkId + memo +
|
||||
* contactId + name)` and the produced signature is stored back into [AddressEntry.signature].
|
||||
*/
|
||||
class SignAddressEntriesUseCase(
|
||||
private val signHashesUseCase: SignHashesUseCase,
|
||||
private val signUseCase: SignUseCase,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWallet: UserWallet, contact: Contact): Either<SignHashesError, Contact> = either {
|
||||
val entries = contact.addressEntries
|
||||
if (entries.isEmpty()) return@either contact
|
||||
|
||||
val publicKey = userWallet.primarySecp256k1PublicKey() ?: raise(SignHashesError.NoSigningKey)
|
||||
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 ->
|
||||
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.models.wallet.UserWallet
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
@ -23,7 +23,7 @@ import com.tangem.utils.extensions.hexToBytesOrNull
|
|||
* Each entry is verified against the exact bytes that were signed (see [buildAddressEntryPayload]).
|
||||
*/
|
||||
class VerifyAddressEntriesUseCase(
|
||||
private val verifyMessagesUseCase: VerifyMessagesUseCase,
|
||||
private val verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.usecase
|
|||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
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.AddressEntryId
|
||||
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.UserWalletId
|
||||
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 io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -28,14 +30,16 @@ import java.security.MessageDigest
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class SignAddressEntriesUseCaseTest {
|
||||
|
||||
private val signHashesUseCase: SignHashesUseCase = mockk()
|
||||
private val useCase = SignAddressEntriesUseCase(signHashesUseCase = signHashesUseCase)
|
||||
private val signUseCase: SignUseCase = mockk()
|
||||
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
|
||||
fun resetMocks() {
|
||||
clearMocks(signHashesUseCase)
|
||||
clearMocks(signUseCase)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -47,7 +51,10 @@ class SignAddressEntriesUseCaseTest {
|
|||
)
|
||||
val signatures = listOf(byteArrayOf(0x01, 0xAB.toByte()), byteArrayOf(0xCD.toByte()))
|
||||
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
|
||||
val result = useCase(userWallet, contact)
|
||||
|
|
@ -61,6 +68,8 @@ class SignAddressEntriesUseCaseTest {
|
|||
),
|
||||
)
|
||||
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
|
||||
assertThat(hashesSlot.captured.map { it.toHexString() })
|
||||
.containsExactly(
|
||||
|
|
@ -80,20 +89,35 @@ class SignAddressEntriesUseCaseTest {
|
|||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()).isEqualTo(contact)
|
||||
coVerify(exactly = 0) { signHashesUseCase(any(), any()) }
|
||||
coVerify(exactly = 0) { signUseCase(any<List<ByteArray>>(), any(), any()) }
|
||||
}
|
||||
|
||||
@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
|
||||
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
|
||||
val result = useCase(userWallet, contact)
|
||||
|
||||
// Assert
|
||||
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.NoSigningKey)
|
||||
assertThat(result.leftOrNull()).isEqualTo(SignHashesError.SigningFailed(message = "canceled"))
|
||||
}
|
||||
|
||||
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.UserWalletId
|
||||
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 io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
|
|
@ -26,7 +26,7 @@ import org.junit.jupiter.api.TestInstance
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class VerifyAddressEntriesUseCaseTest {
|
||||
|
||||
private val verifyMessagesUseCase: VerifyMessagesUseCase = mockk()
|
||||
private val verifyMessagesUseCase: VerifySecp256k1MessagesUseCase = mockk()
|
||||
private val useCase = VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
||||
|
||||
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 is locked or has no secp256k1 key.
|
||||
*
|
||||
* This is the single source of truth for the key used to sign ([SignHashesUseCase]) and verify
|
||||
* ([VerifyMessagesUseCase]) raw hashes, so both operations resolve to the very same key.
|
||||
* This is the single source of truth for the key a caller hands to [SignUseCase] and the 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
|
||||
.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||
?.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.right
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
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.network.Network
|
||||
import com.tangem.domain.transaction.error.SignHashesError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
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 {
|
||||
val card = userWallet.scanResponse.card
|
||||
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
|
||||
* 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
|
||||
* SHA-256 before the elliptic-curve operation, so verification applies the same SHA-256 internally
|
||||
* (via [CryptoUtils.verify]). [messages] and [signatures] are positional — element `i` of one must
|
||||
* correspond to element `i` of the other.
|
||||
* Only secp256k1 is supported: the verifier hashes the message with SHA-256 and runs ECDSA, which
|
||||
* mirrors how the card signs (`SHA-256(message)` then ECDSA). Other curves use a different hashing
|
||||
* scheme, so this use case is deliberately curve-specific rather than parameterized.
|
||||
*
|
||||
* 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
|
||||
* `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
|
||||
* be verified) rather than a list of `false`s.
|
||||
*/
|
||||
class VerifyMessagesUseCase {
|
||||
class VerifySecp256k1MessagesUseCase {
|
||||
|
||||
operator fun invoke(
|
||||
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
|
||||
|
||||
@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.
|
||||
private val privateKey = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632550".hexToBytes()
|
||||
Loading…
Add table
Add a link
Reference in a new issue