Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-29 17:33:28 +08:00
parent cf7d5814d6
commit 10ba20b045
30 changed files with 358 additions and 264 deletions

View file

@ -4,7 +4,7 @@ import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore
@ -47,7 +47,7 @@ interface ApplicationEntryPoint {
fun getAppStateHolder(): AppStateHolder
fun getAssetReader(): AssetReader
fun getAssetLoader(): AssetLoader
fun getFeatureTogglesManager(): FeatureTogglesManager

View file

@ -18,7 +18,7 @@ import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
@ -92,8 +92,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val configManager: ConfigManager
get() = entryPoint.getConfigManager()
private val assetReader: AssetReader
get() = entryPoint.getAssetReader()
private val assetLoader: AssetLoader
get() = entryPoint.getAssetLoader()
private val featureTogglesManager: FeatureTogglesManager
get() = entryPoint.getFeatureTogglesManager()
@ -217,10 +217,12 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
initConfigManager(
loader = FeaturesLocalLoader(assetLoader, BuildConfig.ENVIRONMENT),
onComplete = ::initWithConfigDependency,
)
}
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
loadNativeLibraries()
@ -290,7 +292,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
System.loadLibrary("TrustWalletCore")
}
private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
private suspend fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
configManager.load(loader) { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager))
onComplete(config)

View file

@ -1,9 +1,7 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
@ -23,15 +21,13 @@ internal object WalletManagersFacadeModule {
fun provideWalletManagersFacade(
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
assetLoader: AssetLoader,
blockchainSDKFactory: BlockchainSDKFactory,
): WalletManagersFacade {
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
assetReader = assetReader,
moshi = moshi,
assetLoader = assetLoader,
blockchainSDKFactory = blockchainSDKFactory,
)
}

View file

@ -1,40 +1,36 @@
package com.tangem.tap.domain.twins
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.flow.MutableStateFlow
class TwinCardsManager(
card: CardDTO,
assetReader: AssetReader,
) {
class TwinCardsManager(card: CardDTO) {
private val firstCardId: String = card.cardId
private val publicKey: String = card.issuer.publicKey.toHexString()
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
private val issuerKeyPairFlow = MutableStateFlow<KeyPair?>(value = null)
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
val response = tangemSdkManager.createFirstTwinWallet(
cardId = firstCardId,
initialMessage = message,
)
when (response) {
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
is CompletionResult.Failure -> {}
val response = tangemSdkManager.createFirstTwinWallet(cardId = firstCardId, initialMessage = message)
if (response is CompletionResult.Success) {
currentCardPublicKey = response.data.wallet.publicKey.toHexString()
}
return response
}
@ -46,58 +42,51 @@ class TwinCardsManager(
val response = tangemSdkManager.createSecondTwinWallet(
firstPublicKey = currentCardPublicKey!!,
firstCardId = firstCardId,
issuerKeys = issuerKeyPair,
issuerKeys = getIssuerKeys(),
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage,
initialMessage = initialMessage,
)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
}
is CompletionResult.Failure -> {}
if (response is CompletionResult.Success) {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
}
return response
}
suspend fun complete(message: Message): Result<ScanResponse> {
val response = tangemSdkManager.finalizeTwin(
secondCardPublicKey = secondCardPublicKey!!.hexToBytes(),
issuerKeyPair = issuerKeyPair,
issuerKeyPair = getIssuerKeys(),
cardId = firstCardId,
initialMessage = message,
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> Result.fromTangemSdkError(response.error)
}
}
companion object {
private fun getIssuerKeys(reader: AssetReader, publicKey: String): KeyPair {
val issuer = getIssuers(reader).first { it.publicKey == publicKey }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes(),
)
}
private suspend fun getIssuerKeys(): KeyPair {
issuerKeyPairFlow.value?.let { return it }
private fun getAdapter(): JsonAdapter<List<Issuer>> {
return MoshiConverter.sdkMoshi.adapter(
Types.newParameterizedType(List::class.java, Issuer::class.java),
)
}
val assetLoader = store.inject(DaggerGraphState::assetLoader)
val issuer = assetLoader.loadList<Issuer>(fileName = ISSUERS_FILE_NAME)
.first { it.publicKey == publicKey }
@Deprecated(message = "Use AssetReader instead")
private fun getIssuers(reader: AssetReader): List<Issuer> {
val file = reader.readJson(fileName = "tangem-app-config/issuers")
return getAdapter().fromJson(file)!!
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes(),
).also {
issuerKeyPairFlow.value = it
}
}
private companion object {
const val ISSUERS_FILE_NAME = "tangem-app-config/issuers"
}
}
private class Issuer(
val privateKey: String,
val publicKey: String,
)
private class Issuer(val privateKey: String, val publicKey: String)

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
@ -27,7 +26,7 @@ sealed class TwinCardsAction : Action {
}
sealed class Wallet : TwinCardsAction() {
data class LaunchFirstStep(val initialMessage: Message, val reader: AssetReader) : TwinCardsAction()
data class LaunchFirstStep(val initialMessage: Message) : TwinCardsAction()
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,

View file

@ -165,10 +165,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
val manager = TwinCardsManager(
card = getScanResponse().card,
assetReader = action.reader,
)
val manager = TwinCardsManager(card = getScanResponse().card)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {

View file

@ -15,7 +15,6 @@ import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.Artwork
@ -35,15 +34,11 @@ import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@Suppress("LargeClass")
@AndroidEntryPoint
internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>() {
@Inject
lateinit var assetReader: AssetReader
private val mainBinding by lazy { binding.vMain }
private var previousStep: TwinCardsStep = TwinCardsStep.None
@ -260,7 +255,6 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>(
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
reader = assetReader,
),
)
}

View file

@ -4,6 +4,7 @@ import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.navigation.email.EmailSender
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -77,6 +78,7 @@ data class DaggerGraphState(
val blockchainSDKFactory: BlockchainSDKFactory? = null,
val emailSender: EmailSender? = null,
val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null,
val assetLoader: AssetLoader? = null,
val detailsFeatureToggles: DetailsFeatureToggles? = null,
val detailsEntryPoint: DetailsEntryPoint? = null,
) : StateType

View file

@ -69,4 +69,9 @@ dependencies {
implementation(deps.room.runtime)
implementation(deps.room.ktx)
kapt(deps.room.compiler)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -5,27 +5,31 @@ import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Asset file loader
*
* @property assetReader asset reader
* @property moshi moshi
*
* @see <a href = "https://www.notion.so/tangem/Assets-e045dd890413413faf34ce07ae47ff56">Documentation</a>
*
[REDACTED_AUTHOR]
*/
@Singleton
class AssetLoader @Inject constructor(
val assetReader: AssetReader,
@NetworkMoshi val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
) {
/** Load content [Content] of asset file [fileName] */
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun <reified Content> load(fileName: String): Content? {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
moshi.adapter<Content>().fromJson(json)
}
@ -43,8 +47,8 @@ class AssetLoader @Inject constructor(
/** Load list [V] values of asset file [fileName] */
suspend inline fun <reified V> loadList(fileName: String): List<V> {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
@ -65,8 +69,8 @@ class AssetLoader @Inject constructor(
/** Load map [String] keys and [V] values of asset file [fileName] */
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)

View file

@ -1,27 +1,23 @@
package com.tangem.datasource.asset.reader
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import android.content.res.AssetManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStream
import javax.inject.Inject
/**
* Implementation of asset file reader
*
* @property context application context
* @property assetManager asset manager
* @property dispatchers dispatchers
*/
internal class AndroidAssetReader @Inject constructor(
@ApplicationContext private val context: Context,
internal class AndroidAssetReader(
private val assetManager: AssetManager,
private val dispatchers: CoroutineDispatcherProvider,
) : AssetReader {
override fun readJson(fileName: String): String {
return openFile("$fileName.json")
.bufferedReader()
override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) {
assetManager.open(fullFileName).bufferedReader()
.use(BufferedReader::readText)
}
override fun openFile(file: String): InputStream {
return context.assets.open(file)
}
}

View file

@ -1,17 +1,18 @@
package com.tangem.datasource.asset.reader
import java.io.InputStream
/**
* Asset file reader
*
* @see <a href = "https://www.notion.so/tangem/Assets-e045dd890413413faf34ce07ae47ff56">Documentation</a>
*
[REDACTED_AUTHOR]
*/
interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
/** Open a file [file] from asset as InputStream */
fun openFile(file: String): InputStream
/**
* Read content of file from assets
*
* @param fullFileName name of file with extension. Example: file.json
*/
suspend fun read(fullFileName: String): String
}

View file

@ -10,7 +10,7 @@ interface ConfigManager {
val config: Config
fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null)
suspend fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null)
fun turnOff(name: String)

View file

@ -19,7 +19,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
private var defaultConfig = Config()
override fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)?) {
override suspend fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)?) {
configLoader.load { configModel ->
setupFeature(configModel.features)
setupConfigValues(configModel.configValues)

View file

@ -1,39 +1,25 @@
package com.tangem.datasource.config
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AssetReader instead")
class FeaturesLocalLoader(
private val assetReader: AssetReader,
private val moshi: Moshi,
private val assetLoader: AssetLoader,
buildEnvironment: String,
) : Loader<ConfigModel> {
private val featuresName = "features_$buildEnvironment"
private val configValuesName = "tangem-app-config/config_$buildEnvironment"
override fun load(onComplete: (ConfigModel) -> Unit) {
val config = try {
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = assetReader.readJson(featuresName)
val jsonConfigValues = assetReader.readJson(configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {
Timber.e(ex)
ConfigModel.empty()
}
onComplete(config)
override suspend fun load(onComplete: (ConfigModel) -> Unit) {
ConfigModel(
features = assetLoader.load<FeatureModel>(fileName = featuresName),
configValues = assetLoader.load<ConfigValueModel>(fileName = configValuesName),
).also(onComplete)
}
}

View file

@ -4,5 +4,5 @@ package com.tangem.datasource.config
[REDACTED_AUTHOR]
*/
interface Loader<T> {
fun load(onComplete: (T) -> Unit)
suspend fun load(onComplete: (T) -> Unit)
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AssetModule {
@Binds
@Singleton
fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader
}

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 AssetReaderModule {
@Singleton
@Provides
fun providesAsserReader(
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): AssetReader {
return AndroidAssetReader(context.assets, dispatchers)
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import dagger.Module
@ -16,12 +14,11 @@ import javax.inject.Singleton
*/
@Module
@InstallIn(SingletonComponent::class)
object TestnetTokensStorageModule {
internal object TestnetTokensStorageModule {
@OptIn(ExperimentalStdlibApi::class)
@Provides
@Singleton
fun providesTestnetTokensStorage(assetReader: AssetReader, @SdkMoshi moshi: Moshi): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetReader = assetReader, adapter = moshi.adapter())
fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetLoader)
}
}

View file

@ -1,29 +1,21 @@
package com.tangem.datasource.local.testnet
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
/**
* Default implementation for storing testnet tokens data
*
* @property assetReader file reader from assets
* @property adapter adapter for parsing testnet tokens config
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class DefaultTestnetTokensStorage(
private val assetReader: AssetReader,
private val adapter: JsonAdapter<TestnetTokensConfig>,
private val assetLoader: AssetLoader,
) : TestnetTokensStorage {
@Deprecated(message = "Use AssetReader instead")
override fun getConfig(): TestnetTokensConfig {
return requireNotNull(
value = adapter.fromJson(
assetReader.readJson(fileName = LOCAL_CONFIG_PATH),
),
)
override suspend fun getConfig(): TestnetTokensConfig {
return requireNotNull(assetLoader.load<TestnetTokensConfig>(fileName = LOCAL_CONFIG_PATH))
}
private companion object {

View file

@ -10,5 +10,5 @@ import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
interface TestnetTokensStorage {
/** Get a testnet tokens data */
fun getConfig(): TestnetTokensConfig
suspend fun getConfig(): TestnetTokensConfig
}

View file

@ -0,0 +1,119 @@
package com.tangem.datasource.asset.loader
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.asset.reader.AssetReader
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalStdlibApi::class)
class AssetLoaderTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
@Test
fun load() = runTest {
everyReadingJson() returns assetObjectJson
val jsonAdapter = mockk<JsonAdapter<Asset>>()
every { moshi.adapter<Asset>() } returns jsonAdapter
every { jsonAdapter.fromJson(assetObjectJson) } returns assetObject
val actual = assetLoader.load<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetObjectJson)
}
Truth.assertThat(actual).isEqualTo(assetObject)
}
@Test
fun loadList() = runTest {
everyReadingJson() returns assetListJson
val jsonAdapter = mockk<JsonAdapter<List<Asset>>>()
val types = Types.newParameterizedType(List::class.java, Asset::class.java)
every { moshi.adapter<List<Asset>>(types) } returns jsonAdapter
every { jsonAdapter.fromJson(assetListJson) } returns assetList
val actual = assetLoader.loadList<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetListJson)
}
Truth.assertThat(actual).isEqualTo(assetList)
}
@Test
fun loadMap() = runTest {
everyReadingJson() returns assetMapJson
val jsonAdapter = mockk<JsonAdapter<Map<String, Asset>>>()
val types = Types.newParameterizedType(Map::class.java, String::class.java, Asset::class.java)
every { moshi.adapter<Map<String, Asset>>(types) } returns jsonAdapter
every { jsonAdapter.fromJson(assetMapJson) } returns assetMap
val actual = assetLoader.loadMap<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetMapJson)
}
Truth.assertThat(actual).isEqualTo(assetMap)
}
private fun everyReadingJson() = coEvery { assetReader.read("$JSON_FILE_NAME.json") }
private companion object {
const val JSON_FILE_NAME = "config"
val assetObject = Asset(
contractAddress = "0x0000000000000000000000000000000000000000",
network = "Network",
exchangeAvailable = true,
)
val assetList = listOf(assetObject, assetObject)
val assetMap = mapOf("key1" to assetObject, "key2" to assetObject)
val assetObjectJson = """
{
"contractAddress": "${assetObject.contractAddress}",
"network": "${assetObject.network}",
"exchangeAvailable": ${assetObject.exchangeAvailable}
}
""".trimIndent()
val assetListJson = """
$assetObjectJson,
$assetObjectJson
""".trimIndent()
val assetMapJson = """
{
"key1": $assetObjectJson,
"key2": $assetObjectJson
}
""".trimIndent()
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.google.common.truth.Truth
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.IOException
/**
[REDACTED_AUTHOR]
*/
internal class AndroidAssetReaderTest {
private val assetManager = mockk<AssetManager>()
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
@Test
fun read_content() = runTest {
every { assetManager.open(FILE_NAME) } returns json.byteInputStream()
val actual = assetReader.read(fullFileName = FILE_NAME)
Truth.assertThat(actual).isEqualTo(json)
}
@Test
fun read_error() = runTest {
val exception = IOException("Error")
every { assetManager.open(FILE_NAME) } throws exception
runCatching { assetReader.read(fullFileName = FILE_NAME) }
.onSuccess { throw IllegalStateException("Error should be thrown") }
.onFailure {
Truth.assertThat(it).isEqualTo(exception)
}
}
private companion object {
const val FILE_NAME = "file.json"
val json = """
{
"key": "value"
}
""".trimIndent()
}
}

View file

@ -26,6 +26,7 @@ dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)

View file

@ -1,16 +1,13 @@
package com.tangem.core.featuretoggle.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
@ -25,17 +22,12 @@ internal object FeatureTogglesManagerModule {
@Provides
@Singleton
@OptIn(ExperimentalStdlibApi::class)
fun provideFeatureTogglesManager(
@ApplicationContext context: Context,
assetReader: AssetReader,
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
assetReader = assetReader,
jsonAdapter = moshi.adapter(),
)
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context)
return if (BuildConfig.TESTER_MENU_ENABLED) {

View file

@ -1,42 +1,28 @@
package com.tangem.core.featuretoggle.storage
import androidx.annotation.VisibleForTesting
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
import com.tangem.datasource.asset.reader.AssetReader
import timber.log.Timber
import com.tangem.datasource.asset.loader.AssetLoader
import kotlin.properties.Delegates
/**
* Storage implementation for storing local feature toggles.
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
*
* @property assetReader asset reader
* @property jsonAdapter adapter for parsing local json config
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class LocalFeatureTogglesStorage(
private val assetReader: AssetReader,
private val jsonAdapter: JsonAdapter<List<FeatureToggle>>,
private val assetLoader: AssetLoader,
) : FeatureTogglesStorage {
override var featureToggles: List<FeatureToggle> by Delegates.notNull()
private set
@Deprecated(message = "Use AssetReader instead")
override suspend fun init() {
runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) }
.onSuccess { featureToggles = it }
.onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") }
featureToggles = assetLoader.loadList<FeatureToggle>(LOCAL_CONFIG_PATH)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getConfigPath() = LOCAL_CONFIG_PATH
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getFeatureToggles(): Iterable<FeatureToggle> = featureToggles
private companion object {
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
}

View file

@ -3,11 +3,11 @@ package com.tangem.core.featuretoggle.storage
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.verifyAll
import io.mockk.verifyOrder
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.IOException
@ -19,70 +19,69 @@ import java.io.IOException
internal class LocalFeatureTogglesStorageTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val jsonAdapter = mockk<JsonAdapter<List<FeatureToggle>>>()
private val storage = LocalFeatureTogglesStorage(assetReader, jsonAdapter)
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val storage = LocalFeatureTogglesStorage(assetLoader)
@Test
fun `successfully initialize storage`() = runTest {
coEvery { assetReader.readJson(storage.getConfigPath()) } returns json
coEvery { jsonAdapter.fromJson(json) } returns featureToggles
everyReadingJson() returns json
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() returns featureToggles
storage.init()
verifyOrder {
assetReader.readJson(storage.getConfigPath())
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
Truth.assertThat(storage.getFeatureToggles()).containsExactlyElementsIn(featureToggles)
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(featureToggles)
}
@Test
fun `failure initialize storage if assetReader throws exception`() = runTest {
coEvery { assetReader.readJson(storage.getConfigPath()) } returns json
coEvery { jsonAdapter.fromJson(json) } throws IOException()
everyReadingJson() returns json
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() throws IOException()
storage.init()
verifyOrder {
assetReader.readJson(storage.getConfigPath())
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
runCatching { storage.getFeatureToggles() }
.onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") }
.onFailure {
Truth
.assertThat(it)
.hasMessageThat()
.contains("Property featureToggles should be initialized before get.")
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
}
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList<FeatureToggle>())
}
@Test
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
coEvery { assetReader.readJson(storage.getConfigPath()) } throws IOException()
everyReadingJson() throws IOException()
storage.init()
verifyOrder { assetReader.readJson(storage.getConfigPath()) }
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
runCatching { storage.getFeatureToggles() }
.onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") }
.onFailure {
Truth
.assertThat(it)
.hasMessageThat()
.contains("Property featureToggles should be initialized before get.")
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
}
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList<FeatureToggle>())
}
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
private fun everyCreatingMoshiAdapter() = every {
val types = Types.newParameterizedType(List::class.java, FeatureToggle::class.java)
moshi.adapter<List<FeatureToggle>>(types)
}
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
private companion object {
const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json"
val json = """
[

View file

@ -4,7 +4,6 @@ import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
@ -19,7 +18,7 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.common.util.hasDerivation
@ -31,6 +30,7 @@ import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
import com.tangem.domain.walletmanager.model.SmartContractMethod
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.walletmanager.utils.*
import com.tangem.domain.walletmanager.utils.WalletManagerFactory
@ -47,8 +47,7 @@ import java.util.EnumSet
class DefaultWalletManagersFacade(
private val walletManagersStore: WalletManagersStore,
private val userWalletsStore: UserWalletsStore,
assetReader: AssetReader,
moshi: Moshi,
private val assetLoader: AssetLoader,
blockchainSDKFactory: BlockchainSDKFactory,
) : WalletManagersFacade {
@ -57,7 +56,6 @@ class DefaultWalletManagersFacade(
private val walletManagerFactory by lazy { WalletManagerFactory(blockchainSDKFactory) }
private val sdkTokenConverter by lazy { SdkTokenConverter() }
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }
private val sdkPageConverter by lazy { SdkPageConverter() }
private val cryptoCurrencyTypeConverter by lazy { CryptoCurrencyTypeConverter() }
private val requirementsConditionConverter by lazy { SdkRequirementsConditionConverter() }
@ -236,7 +234,8 @@ class DefaultWalletManagersFacade(
is Result.Success -> PaginationWrapper(
currentPage = sdkPageConverter.convert(page),
nextPage = sdkPageConverter.convert(itemsResult.data.nextPage),
items = txHistoryItemConverter.convertList(itemsResult.data.items),
items = SdkTransactionHistoryItemConverter(smartContractMethods = readSmartContractMethods())
.convertList(itemsResult.data.items),
)
is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage)
}
@ -616,4 +615,8 @@ class DefaultWalletManagersFacade(
walletManager.addTokens(tokensToAdd)
}
private suspend fun readSmartContractMethods(): Map<String, SmartContractMethod> {
return assetLoader.loadMap<SmartContractMethod>(fileName = "contract_methods")
}
}

View file

@ -1,20 +1,16 @@
package com.tangem.domain.walletmanager.utils
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.SmartContractMethod
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem as SdkTransactionHistoryItem
internal class SdkTransactionHistoryItemConverter(
assetReader: AssetReader,
moshi: Moshi,
smartContractMethods: Map<String, SmartContractMethod>,
) : Converter<SdkTransactionHistoryItem, TxHistoryItem> {
private val typeConverter by lazy {
SdkTransactionTypeConverter(assetReader, moshi)
}
private val typeConverter by lazy { SdkTransactionTypeConverter(smartContractMethods) }
override fun convert(value: SdkTransactionHistoryItem): TxHistoryItem = TxHistoryItem(
txHash = value.txHash,

View file

@ -1,30 +1,14 @@
package com.tangem.domain.walletmanager.utils
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.SmartContractMethod
import com.tangem.utils.converter.Converter
internal class SdkTransactionTypeConverter(
private val assetReader: AssetReader,
private val moshi: Moshi,
private val smartContractMethods: Map<String, SmartContractMethod>,
) : Converter<TransactionHistoryItem.TransactionType, TxHistoryItem.TransactionType> {
private val adapter: JsonAdapter<Map<String, SmartContractMethod>> by lazy {
moshi.adapter(
Types.newParameterizedType(
Map::class.java,
String::class.java,
SmartContractMethod::class.java,
),
)
}
private val smartContractMethods by lazy { readSmartContractMethods() }
override fun convert(value: TransactionHistoryItem.TransactionType): TxHistoryItem.TransactionType {
return when (value) {
is TransactionHistoryItem.TransactionType.ContractMethod ->
@ -43,9 +27,4 @@ internal class SdkTransactionTypeConverter(
else -> TxHistoryItem.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() })
}
}
private fun readSmartContractMethods(): Map<String, SmartContractMethod> {
val json = assetReader.readJson("contract_methods")
return adapter.fromJson(json) ?: emptyMap()
}
}