From 0f600e9dcfb5f8f0105a46880eebbecf0c1119ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Mar 2024 23:32:11 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/QrScanningDomainModule.kt | 7 + .../tap/features/send/ui/SendFragment.kt | 32 ++- data/qr-scanning/build.gradle.kts | 12 + .../DefaultQrScanningEventsRepository.kt | 123 +++++++++ .../DefaultQrScanningEventsRepositoryTest.kt | 253 ++++++++++++++++++ .../domain/common/extensions/Blockchain.kt | 16 ++ domain/qr-scanning/build.gradle.kts | 8 +- .../domain/qrscanning/models/QrResult.kt | 9 + .../repository/QrScanningEventsRepository.kt | 4 + .../qrscanning/usecases/ParseQrCodeUseCase.kt | 20 ++ .../presentation/viewmodel/SendViewModel.kt | 14 +- gradle/dependencies.toml | 2 +- 12 files changed, 489 insertions(+), 11 deletions(-) create mode 100644 data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt create mode 100644 domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt create mode 100644 domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt index 43f66670c6..ec86de8c46 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,4 +25,10 @@ internal object QrScanningDomainModule { fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase { return EmitQrScannedEventUseCase(repository) } + + @Provides + @Singleton + fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase { + return ParseQrCodeUseCase(repository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 84628d0ffa..0402c5e2de 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -25,10 +25,14 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -60,6 +64,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import java.text.DecimalFormatSymbols import javax.inject.Inject @@ -82,11 +87,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) private lateinit var keyboardObserver: KeyboardObserver + private val cryptoCurrency: CryptoCurrency? + get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY) + val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind) @Inject lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase + @Inject + lateinit var parseQrCodeUseCase: ParseQrCodeUseCase + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) @@ -177,13 +188,21 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } .flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED) - .collect { + .collect { rawQr -> delay(200) // Delayed launch is needed in order for the UI to be drawn and to process the sent events. // If do not use the delay, then etAmount error field is not displayed when // inserting an incorrect amount by shareUri - onCodeScanned(it) + cryptoCurrency?.let { cryptoCurrency -> + parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold( + ifLeft = { + onCodeScanned(QrResult(address = rawQr)) + Timber.w(it) + }, + ifRight = { onCodeScanned(it) }, + ) + } ?: onCodeScanned(QrResult(address = rawQr)) } } } @@ -254,15 +273,18 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(mainScope) } - private fun onCodeScanned(scannedCode: String) { - if (scannedCode.isEmpty()) return + private fun onCodeScanned(parsedQr: QrResult) { + if (parsedQr.address.isEmpty()) return store.dispatch( PasteAddress( - data = scannedCode, + data = parsedQr.address, sourceType = Token.Send.AddressEntered.SourceType.QRCode, ), ) + parsedQr.amount?.let { amount -> + store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false)) + } store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused)) } diff --git a/data/qr-scanning/build.gradle.kts b/data/qr-scanning/build.gradle.kts index ec2f620228..1c801589b7 100644 --- a/data/qr-scanning/build.gradle.kts +++ b/data/qr-scanning/build.gradle.kts @@ -15,8 +15,20 @@ dependencies { /** Domain */ implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) + + implementation(projects.core.ui) + + /** SdK */ + implementation(deps.tangem.blockchain) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 1732996a6b..87729e8bc4 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -1,10 +1,15 @@ package com.tangem.data.qrscanning.repository +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import java.math.BigDecimal +import java.net.URLDecoder internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { @@ -19,4 +24,122 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { override fun subscribeToScanningResults(type: SourceType) = scannedEvents .filter { it.type == type } .map { it.qrCode } + + override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult { + val withoutSchema = stripSchema(qrCode, cryptoCurrency) + + // A poor man's ERC-681 parser: we want to extract only the destination address, and we don't care + // about other parts of the ERC-681 payload string like `chain_id` and/or `function_name`. + // + // We're extracting the destination address by parsing the given string until we meet + // any of the possible string delimiters (@ ? /). + val address = withoutSchema.takeWhile { char -> + char != CHAIN_DELIMITER && char != FUNCTION_DELIMITER && char != PARAM_DELIMITER + } + + val result = QrResult(address = address) + + extractParameters(withoutSchema) + .forEach { + when (it.key) { + Parameter.Amount -> { + // According to BIP-0021, the value is specified in decimals. No conversion needed + result.amount = it.value.toBigDecimalOrNull() + } + Parameter.Message, + Parameter.Memo, + -> { + result.memo = URLDecoder.decode(it.value, "UTF-8") + } + Parameter.Address -> { + // Overrides destination address for token transfers (ERC-681) + if (cryptoCurrency is CryptoCurrency.Token) { + // `address` parameter is used only if the contract address, encoded in the QR, + // matches the contract address of the token. + // Otherwise, the scanned string is likely malformed, and we stop the entire parsing routine + if (cryptoCurrency.contractAddress.equals(address, ignoreCase = true)) { + result.address = it.value + } else { + return QrResult() + } + } + } + Parameter.Value, + Parameter.Uint256, + -> { + // Extra convert parses scientific notation to decimal + // This is necessary to be able comparing BigDecimal values + result.amount = it.value.toBigDecimalOrNull() + ?.toPlainString()?.toBigDecimalOrNull() + ?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals)) + } + } + } + + return result + } + + private fun stripSchema(raw: String, currency: CryptoCurrency): String { + val qrSchemas = Blockchain.fromId(currency.network.id.value).getShareScheme() + + // The most specific (i.e. the most lengthy) prefixes always come first + qrSchemas + .sortedByDescending { it.length } + .forEach { schema -> + val stripped = raw.split(schema) + + if (stripped.size > 1) return stripped.last() + } + + return raw + } + + private fun extractParameters(from: String): Map { + val parametersBlock = from.substringAfter(PARAM_DELIMITER) + if (parametersBlock.isBlank()) return emptyMap() + + val paramList = parametersBlock.split(PARAMS_DELIMITER) + .mapNotNull { param -> + val parameterWithValue = param.split(PARAM_VALUE_DELIMITER) + if (parameterWithValue.size == 2) { + val name = Parameter.getParam(parameterWithValue.first()) + val value = parameterWithValue.last() + + if (name != null) { + name to value + } else { + null + } + } else { + null + } + }.associate { it } + + return paramList + } + + private enum class Parameter { + Amount, + Message, + Memo, + Address, + Value, + Uint256, + ; + + companion object { + fun getParam(name: String): Parameter? { + return Parameter.entries.firstOrNull { it.name.equals(name, ignoreCase = true) } + } + } + } + + private companion object { + // See https://eips.ethereum.org/EIPS/eip-681 for details. + const val CHAIN_DELIMITER = '@' // ERC-681 [ "@" chain_id ] + const val FUNCTION_DELIMITER = '/' // ERC-681 [ "/" function_name ] + const val PARAM_DELIMITER = '?' // BIP-021, ERC-681 + const val PARAMS_DELIMITER = '&' + const val PARAM_VALUE_DELIMITER = '=' + } } \ No newline at end of file diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt new file mode 100644 index 0000000000..ad1f6244a7 --- /dev/null +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt @@ -0,0 +1,253 @@ +package com.tangem.data.qrscanning + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import java.math.BigDecimal + +internal class DefaultQrScanningEventsRepositoryTest { + + private val repository = DefaultQrScanningEventsRepository() + + private val cryptoCurrencyId = mockk() + private val network = mockk() + private val cryptoCurrency = CryptoCurrency.Coin( + id = cryptoCurrencyId, + network = network, + name = "blockchain", + symbol = "symbol", + decimals = 18, + iconUrl = null, + isCustom = false, + ) + private val tokenCryptoCurrency = CryptoCurrency.Token( + id = cryptoCurrencyId, + network = network, + name = "blockchain", + symbol = "symbol", + decimals = 7, + iconUrl = null, + isCustom = false, + contractAddress = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7", + ) + + private val garbage = "some_garbage=" + + private val schema1 = "bitcoin" + private val schema2 = "ethereum" + + private val address1 = "bc1pw83rs5s75na2g7ec8yqgekr3ae209ye7ck2ftakjnh8tv3xzw8ls6wgt62" + private val address2 = "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + private val address3 = "pay-0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + private val address4 = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7" + + private val function = "/transfer" + + private val someParam = "someParam" + private val someParamValue = "someParamValue" + + private val addressParam = "address" + private val addressParamValue = "0xc00f86ab93cd0bd3a60213583d0fe35aaa1ace23" + + private val amountParam = "amount" + private val someAmountParamValue = "amount" + private val amountParamValue = "123.123" + + private val valueParam = "value" + private val someValueParamValue = "amount" + private val valueParamValue2 = "1.88e10" + private val valueParamValue3 = "1.68E11" + private val valueParamValue4 = "23000000000" + + private val memoParam = "memo" + private val memoParamValue = "a%20random%20memo" + private val memoParamValueUtf8 = "a random memo" + + private val messageParam = "message" + private val messageParamValue = "some%20message" + private val messageParamValueUft8 = "some message" + private val messageParamValue2 = "message" + + @Test + fun testBip021() { + every { network.id } returns Network.ID(Blockchain.Bitcoin.id) + positiveCase( + "$schema1:$address1", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema1:$address1", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$someAmountParamValue", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address1, amount = BigDecimal(amountParamValue)), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue&$memoParam=$memoParamValue", + QrResult(address = address1, amount = BigDecimal(amountParamValue), memo = memoParamValueUtf8), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue", + QrResult(address = address1, memo = messageParamValueUft8), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue2", + QrResult(address = address1, memo = messageParamValue2), + cryptoCurrency, + ) + negativeCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address1, memo = messageParamValue2), + cryptoCurrency, + ) + } + + @Test + fun testErc681Coin() { + every { network.id } returns Network.ID(Blockchain.Ethereum.id) + positiveCase( + address2, + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$schema2:$address2", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$schema2:$address3", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2$function?$addressParam=$addressParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$someAmountParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2", + QrResult(address = address2, amount = BigDecimal("0.0000000188")), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3", + QrResult(address = address2, amount = BigDecimal("0.000000168")), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4", + QrResult(address = address2, amount = BigDecimal("0.000000023")), + cryptoCurrency, + ) + } + + @Test + fun testErc681Token() { + every { network.id } returns Network.ID(Blockchain.Ethereum.id) + positiveCase( + address2, + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address2", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address3", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address4$function?$addressParam=$addressParamValue", + QrResult(address = addressParamValue), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$someValueParamValue", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2", + QrResult(address = address2, amount = BigDecimal("1880")), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3", + QrResult(address = address2, amount = BigDecimal("16800")), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4", + QrResult(address = address2, amount = BigDecimal("2300")), + tokenCryptoCurrency, + ) + negativeCase( + "$address2?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address2, amount = BigDecimal("123.123"), memo = memoParamValueUtf8), + tokenCryptoCurrency, + ) + } + + private fun positiveCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) { + val actual = repository.parseQrCode(input, cryptoCurrency) + Truth.assertThat(actual.address).isEqualTo(expected.address) + Truth.assertThat(actual.amount).isEqualTo(expected.amount) + Truth.assertThat(actual.memo).isEqualTo(expected.memo) + } + + private fun negativeCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) { + val actual = repository.parseQrCode(input, cryptoCurrency) + Truth.assertThat(actual).isNotEqualTo(expected) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 03841cc507..44e60f9690 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -93,6 +93,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "areon-network/test" -> Blockchain.AreonTestnet "pulsechain" -> Blockchain.PulseChain "pulsechain/test" -> Blockchain.PulseChainTestnet + "nexa" -> Blockchain.Nexa // FIXME + "nexa/testnet" -> Blockchain.NexaTestnet // FIXME + "zksync" -> Blockchain.ZkSyncEra // FIXME + "zksync/testnet" -> Blockchain.ZkSyncEraTestnet // FIXME else -> null } } @@ -187,6 +191,10 @@ fun Blockchain.toNetworkId(): String { Blockchain.AreonTestnet -> "areon-network/test" Blockchain.PulseChain -> "pulsechain" Blockchain.PulseChainTestnet -> "pulsechain/test" + Blockchain.ZkSyncEra -> "zksync" // FIXME + Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME + Blockchain.Nexa -> "nexa" // FIXME + Blockchain.NexaTestnet -> "nexa/testnet" // FIXME } } @@ -249,6 +257,10 @@ fun Blockchain.toCoinId(): String { Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-near" Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network" Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain" + Blockchain.ZkSyncEra -> "zksync" // FIXME + Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME + Blockchain.Nexa -> "nexa" // FIXME + Blockchain.NexaTestnet -> "nexa/testnet" // FIXME } } @@ -276,4 +288,8 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Playa3ull, + Blockchain.ZkSyncEra, + Blockchain.ZkSyncEraTestnet, + Blockchain.Nexa, + Blockchain.NexaTestnet, ) \ No newline at end of file diff --git a/domain/qr-scanning/build.gradle.kts b/domain/qr-scanning/build.gradle.kts index fe5d126eb1..f1ea5e63ec 100644 --- a/domain/qr-scanning/build.gradle.kts +++ b/domain/qr-scanning/build.gradle.kts @@ -1,12 +1,18 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.qrscanning" +} + dependencies { /** Domain */ implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt new file mode 100644 index 0000000000..a97a4a7161 --- /dev/null +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.qrscanning.models + +import java.math.BigDecimal + +data class QrResult( + var address: String = "", + var amount: BigDecimal? = null, + var memo: String? = null, +) \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt index b834c4dbcd..15ca99b951 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt @@ -1,6 +1,8 @@ package com.tangem.domain.qrscanning.repository +import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.coroutines.flow.Flow interface QrScanningEventsRepository { @@ -8,4 +10,6 @@ interface QrScanningEventsRepository { suspend fun emitResult(type: SourceType, qrCode: String) fun subscribeToScanningResults(type: SourceType): Flow + + fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult } \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt new file mode 100644 index 0000000000..0849c8bf1c --- /dev/null +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.qrscanning.usecases + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +class ParseQrCodeUseCase( + val repository: QrScanningEventsRepository, +) { + operator fun invoke(qrCode: String, cryptoCurrency: CryptoCurrency): Either { + return try { + repository.parseQrCode(qrCode, cryptoCurrency).right() + } catch (e: Exception) { + e.left() + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index cf90cca81f..0617f6e83b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.* @@ -32,7 +33,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -75,12 +79,12 @@ internal class SendViewModel @Inject constructor( private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, - private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val parseQrCodeUseCase: ParseQrCodeUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -178,6 +182,7 @@ internal class SendViewModel @Inject constructor( private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() + private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() @@ -509,13 +514,14 @@ internal class SendViewModel @Inject constructor( // region recipient state clicks fun onRecipientAddressScanned(address: String) { viewModelScope.launch(dispatchers.main) { - parseSharedAddressUseCase(address, cryptoCurrency.network).fold( + parseQrCodeUseCase(address, cryptoCurrency).fold( ifRight = { parsedCode -> onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) } parsedCode.memo?.let { onRecipientMemoValueChange(it) } }, ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) Timber.w(it) }, ) @@ -543,7 +549,7 @@ internal class SendViewModel @Inject constructor( val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty()) uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress) } - }.saveIn(addressValidationJobHolder) + }.saveIn(memoValidationJobHolder) } private suspend fun validateAddress(value: String): Boolean { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 96db197034..b7e7c03a4e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-514" +tangemBlockchainSdk = "develop-528" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-332" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^