Updated on 2026-08-14
This commit is contained in:
commit
b76e1bbc45
219 changed files with 4585 additions and 5271 deletions
1
data/feedback/.gitignore
vendored
Normal file
1
data/feedback/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
42
data/feedback/build.gradle.kts
Normal file
42
data/feedback/build.gradle.kts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.feedback"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region AndroidX libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Tangem libraries
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
// endregion
|
||||
|
||||
// Other libraries
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
implementation(projects.core.datasource)
|
||||
// endregion
|
||||
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.data.feedback
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Build
|
||||
import com.tangem.data.feedback.converters.BlockchainInfoConverter
|
||||
import com.tangem.data.feedback.converters.CardInfoConverter
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.feedback.models.AppLogModel
|
||||
import com.tangem.domain.feedback.models.BlockchainInfo
|
||||
import com.tangem.domain.feedback.models.CardInfo
|
||||
import com.tangem.domain.feedback.models.PhoneInfo
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
/**
|
||||
* Implementation of [FeedbackRepository]
|
||||
*
|
||||
* @property appPreferencesStore application preferences store
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property context context for getting app version
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultFeedbackRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
private val context: Context,
|
||||
) : FeedbackRepository {
|
||||
|
||||
override suspend fun getCardInfo(): CardInfo {
|
||||
return CardInfoConverter.convert(value = getSelectedUserWallet())
|
||||
}
|
||||
|
||||
override suspend fun getBlockchainInfoList(): List<BlockchainInfo> {
|
||||
return walletManagersStore
|
||||
.getAllSync(userWalletId = getSelectedUserWallet().walletId)
|
||||
.map(BlockchainInfoConverter::convert)
|
||||
}
|
||||
|
||||
override fun getPhoneInfo(): PhoneInfo {
|
||||
return PhoneInfo(
|
||||
phoneModel = Build.MODEL,
|
||||
osVersion = Build.VERSION.SDK_INT.toString(),
|
||||
appVersion = getAppVersion(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAppLogs(): List<AppLogModel> {
|
||||
return appPreferencesStore.getObjectMap<String>(key = PreferencesKeys.APP_LOGS_KEY)
|
||||
.map { AppLogModel(timestamp = it.key.toLong(), message = it.value) }
|
||||
.sortedBy(AppLogModel::timestamp)
|
||||
}
|
||||
|
||||
override suspend fun createLogFile(logs: String): File? {
|
||||
return try {
|
||||
val file = File(context.filesDir, LOGS_FILE)
|
||||
file.delete()
|
||||
file.createNewFile()
|
||||
|
||||
val stringWriter = StringWriter()
|
||||
|
||||
stringWriter.append(logs)
|
||||
|
||||
val fileWriter = FileWriter(file)
|
||||
fileWriter.write(stringWriter.toString())
|
||||
fileWriter.close()
|
||||
|
||||
file
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Logs file isn't created")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppVersion(): String {
|
||||
return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }
|
||||
.fold(
|
||||
onSuccess = PackageInfo::versionName,
|
||||
onFailure = {
|
||||
Timber.e(it)
|
||||
"x.y.z"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSelectedUserWallet(): UserWallet {
|
||||
return userWalletsStore.selectedUserWalletOrNull
|
||||
?: error("UserWallet is not selected")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LOGS_FILE = "logs.txt"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.data.feedback.converters
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.domain.feedback.models.BlockchainInfo
|
||||
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
|
||||
|
||||
/**
|
||||
* Converter from [WalletManager] to [BlockchainInfo]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object BlockchainInfoConverter : Converter<WalletManager, BlockchainInfo> {
|
||||
|
||||
override fun convert(value: WalletManager): BlockchainInfo {
|
||||
return BlockchainInfo(
|
||||
blockchain = value.wallet.blockchain.fullName,
|
||||
derivationPath = value.wallet.publicKey.derivationPath?.rawPath ?: "",
|
||||
outputsCount = value.outputsCount?.toString(),
|
||||
host = value.currentHost,
|
||||
addresses = value.wallet.mapAddresses(Address::value),
|
||||
explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) },
|
||||
tokens = value.cardTokens.map { token ->
|
||||
BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun Wallet.mapAddresses(map: (Address) -> String): BlockchainAddresses {
|
||||
return if (addresses.size == 1) {
|
||||
BlockchainAddresses.Single(value = map(addresses.first()))
|
||||
} else {
|
||||
BlockchainAddresses.Multiple(
|
||||
addresses.map {
|
||||
AddressInfo(type = it.type.name, value = map(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.feedback.converters
|
||||
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.feedback.models.CardInfo
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [UserWallet] to [CardInfo]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object CardInfoConverter : Converter<UserWallet, CardInfo> {
|
||||
|
||||
override fun convert(value: UserWallet): CardInfo {
|
||||
return with(value.scanResponse) {
|
||||
CardInfo(
|
||||
userWalletId = value.walletId.stringValue,
|
||||
cardId = card.cardId,
|
||||
firmwareVersion = card.firmwareVersion.stringValue,
|
||||
cardBlockchain = walletData?.blockchain,
|
||||
signedHashesList = card.wallets.map {
|
||||
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
|
||||
},
|
||||
isStart2Coin = value.scanResponse.card.isStart2Coin,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.data.feedback.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.data.feedback.DefaultFeedbackRepository
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeedbackRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedbackRepository(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
walletManagersStore: WalletManagersStore,
|
||||
@ApplicationContext context: Context,
|
||||
): FeedbackRepository {
|
||||
return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context)
|
||||
}
|
||||
}
|
||||
1
data/qr-scanning/.gitignore
vendored
Normal file
1
data/qr-scanning/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
34
data/qr-scanning/build.gradle.kts
Normal file
34
data/qr-scanning/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.qrscanning"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.qrscanning.di
|
||||
|
||||
import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository
|
||||
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object QrScanningDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQrScanningEventsRepository(): QrScanningEventsRepository {
|
||||
return DefaultQrScanningEventsRepository()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
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 {
|
||||
|
||||
private data class QrScanningEvent(val type: SourceType, val qrCode: String)
|
||||
|
||||
private val scannedEvents = MutableSharedFlow<QrScanningEvent>()
|
||||
|
||||
override suspend fun emitResult(type: SourceType, qrCode: String) {
|
||||
scannedEvents.emit(QrScanningEvent(type, qrCode))
|
||||
}
|
||||
|
||||
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<Parameter, String> {
|
||||
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 = '='
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CryptoCurrency.ID>()
|
||||
private val network = mockk<Network>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,8 @@ package com.tangem.data.settings
|
|||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.settings.models.AppLogsModel
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -36,11 +34,6 @@ internal class DefaultSettingsRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getAppLogs(): List<AppLogsModel> {
|
||||
return appPreferencesStore.getObjectMap<String>(key = PreferencesKeys.APP_LOGS_KEY)
|
||||
.map { AppLogsModel(timestamp = it.key.toLong(), message = it.value) }
|
||||
}
|
||||
|
||||
override suspend fun updateAppLogs(message: String) {
|
||||
val newLogs = DateTime.now().millis.toString() to message
|
||||
|
||||
|
|
@ -51,22 +44,21 @@ internal class DefaultSettingsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteDeprecatedLogs() {
|
||||
val threeDaysAgo = getThreeDaysAgoTimestamp()
|
||||
|
||||
override suspend fun deleteDeprecatedLogs(maxSize: Int) {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val savedLogs = preferences.getObjectMap<String>(PreferencesKeys.APP_LOGS_KEY)
|
||||
|
||||
var sum = 0
|
||||
preferences.setObjectMap(
|
||||
key = PreferencesKeys.APP_LOGS_KEY,
|
||||
value = savedLogs.filterKeys { it.toLong() > threeDaysAgo },
|
||||
value = savedLogs.entries
|
||||
.sortedBy(Map.Entry<String, String>::key)
|
||||
.takeLastWhile {
|
||||
sum += it.value.length
|
||||
sum < maxSize
|
||||
}
|
||||
.associate { it.key to it.value },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreeDaysAgoTimestamp(): Long = DateTime.now().minusDays(THREE_DAYS).millis
|
||||
|
||||
private companion object {
|
||||
const val THREE_DAYS = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.data.tokens.utils.getNetwork
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.extensions.*
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
|
|
@ -71,6 +73,15 @@ internal class DefaultNetworksCompatibilityRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean {
|
||||
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: return false
|
||||
|
||||
return config.primaryCurve(blockchain) == EllipticCurve.Ed25519Slip0010 &&
|
||||
scanResponse.cardTypesResolver.isWallet2()
|
||||
}
|
||||
|
||||
override fun areTokensSupportedByNetwork(networkId: String): Boolean {
|
||||
return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue