Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-18 18:05:18 +08:00
parent e98780ff93
commit 479d142807
283 changed files with 2338 additions and 2730 deletions

View file

@ -12,7 +12,7 @@ object DomainLayer {
var onInitComplete: ((DomainModuleError?) -> Unit)? = null
fun init(data: Map<String, Any?>? = null) {
fun init() {
initActionStateLogger()
onInitComplete?.invoke(null)

View file

@ -48,6 +48,7 @@ data class CardDTO(
backupStatus = BackupStatus.fromSdkStatus(card.backupStatus),
)
@Suppress("ComplexMethod")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CardDTO) return false
@ -120,13 +121,6 @@ data class CardDTO(
val patch: Int,
val type: SdkFirmwareVersion.FirmwareType,
) : Comparable<SdkFirmwareVersion> {
constructor(firmwareVersion: SdkFirmwareVersion) : this(
major = firmwareVersion.major,
minor = firmwareVersion.minor,
patch = firmwareVersion.patch,
type = firmwareVersion.type,
)
val stringValue: String
get() = StringBuilder()
.append("$major.$minor")
@ -134,6 +128,13 @@ data class CardDTO(
.append(type.rawValue ?: "")
.toString()
constructor(firmwareVersion: SdkFirmwareVersion) : this(
major = firmwareVersion.major,
minor = firmwareVersion.minor,
patch = firmwareVersion.patch,
type = firmwareVersion.type,
)
override fun compareTo(other: SdkFirmwareVersion): Int = when {
major != other.major -> major.compareTo(other.major)
minor != other.minor -> minor.compareTo(other.minor)

View file

@ -3,22 +3,21 @@ package com.tangem.domain.common
import com.tangem.domain.features.BuildConfig
object LogConfig {
val imageLoader: Boolean = false
const val imageLoader: Boolean = false
val storeAction: Boolean = BuildConfig.DEBUG
val zendesk: Boolean = false
const val zendesk: Boolean = false
val network: NetworkLogConfig = NetworkLogConfig
val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig
}
object NetworkLogConfig {
val mercuryoService: Boolean = false
val moonPayService: Boolean = false
val paymentologyApiService: Boolean = BuildConfig.DEBUG
const val mercuryoService: Boolean = false
const val moonPayService: Boolean = false
val blockchainSdkNetwork: Boolean = BuildConfig.DEBUG
}
object AnalyticsHandlersLogConfig {
val firebase: Boolean = false
val appsFlyer: Boolean = false
val amplitude: Boolean = false
const val firebase: Boolean = false
const val appsFlyer: Boolean = false
const val amplitude: Boolean = false
}

View file

@ -6,35 +6,8 @@ import com.tangem.common.CardIdRange
import com.tangem.common.contains
object SaltPayWorkaround {
fun tokenFrom(blockchain: Blockchain): Token {
return when (blockchain) {
Blockchain.SaltPay -> Token(
name = "WXDAI",
symbol = "wxDAI",
contractAddress = "0x4200000000000000000000000000000000000006",
decimals = 18,
id = "wrapped-xdai",
)
else -> throw IllegalArgumentException()
}
}
fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId)
fun isWalletCardId(cardId: String): Boolean {
return if (walletCardIds.contains(cardId)) true else walletCardIdRanges.contains(cardId)
}
val visaBatches = listOf(
"AE02",
"AE03",
) + attachTestVisaBatches()
private fun attachTestVisaBatches(): List<String> {
return listOf(
"FF03",
)
}
@Suppress("MagicNumber")
val visaBatches = listOf("AE02", "AE03") + attachTestVisaBatches()
val walletCardIds = listOf(
"AC01000000033503",
@ -67,19 +40,38 @@ object SaltPayWorkaround {
"AC03000000076229",
) + attachTestWalletCardIds()
private fun attachTestWalletCardIds(): List<String> {
return listOf(
"FF04000000000232",
)
}
val walletCardIdRanges = listOf(
CardIdRange("AC05000000000003", "AC05000000023997")!!,
) + attachTestWalletCardIdRanges()
fun tokenFrom(blockchain: Blockchain): Token {
return when (blockchain) {
Blockchain.SaltPay -> Token(
name = "WXDAI",
symbol = "wxDAI",
contractAddress = "0x4200000000000000000000000000000000000006",
decimals = 18,
id = "wrapped-xdai",
)
else -> error("It is not SaltPay")
}
}
fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId)
fun isWalletCardId(cardId: String): Boolean {
return if (walletCardIds.contains(cardId)) true else walletCardIdRanges.contains(cardId)
}
private fun attachTestVisaBatches(): List<String> {
return listOf("FF03")
}
private fun attachTestWalletCardIds(): List<String> {
return listOf("FF04000000000232")
}
private fun attachTestWalletCardIdRanges(): List<CardIdRange> {
return listOf(
CardIdRange("FF04000000000000", "FF04999999999999")!!,
)
return listOf(CardIdRange("FF04000000000000", "FF04999999999999")!!)
}
}

View file

@ -13,10 +13,6 @@ object TapWorkarounds {
private const val TEST_CARD_BATCH = "99FF"
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
}
val CardDTO.isTangemTwins: Boolean
get() = TwinsHelper.getTwinCardNumber(cardId) != null
@ -41,6 +37,7 @@ object TapWorkarounds {
val CardDTO.useOldStyleDerivation: Boolean
get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95"
val CardDTO.derivationStyle: DerivationStyle?
get() = if (!settings.isHDWalletAllowed) {
null
@ -49,7 +46,6 @@ object TapWorkarounds {
} else {
DerivationStyle.NEW
}
val CardDTO.isExcluded: Boolean
get() {
val excludedBatch = excludedBatches.contains(batchId)
@ -60,9 +56,6 @@ object TapWorkarounds {
val CardDTO.isNotSupportedInThatRelease: Boolean
get() = false
fun CardDTO.getTangemNoteBlockchain(): Blockchain? =
tangemNoteBatches[batchId] ?: if (isSaltPay) Blockchain.Gnosis else null
private val tangemNoteBatches = mapOf(
"AB01" to Blockchain.Bitcoin,
"AB02" to Blockchain.Ethereum,
@ -78,16 +71,16 @@ object TapWorkarounds {
"AB12" to Blockchain.Ethereum,
)
private val excludedBatches = listOf("0027", "0030", "0031", "0035")
private val excludedIssuers = listOf("TTM BANK")
fun CardDTO.getTangemNoteBlockchain(): Blockchain? =
tangemNoteBatches[batchId] ?: if (isSaltPay) Blockchain.Gnosis else null
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
}
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null
private val excludedBatches = listOf(
"0027",
"0030",
"0031",
"0035",
)
private val excludedIssuers = listOf(
"TTM BANK",
)
}

View file

@ -2,47 +2,32 @@ package com.tangem.domain.common
import com.tangem.crypto.CryptoUtils
class TwinsHelper {
companion object {
const val TWIN_FILE_NAME = "TwinPublicKey"
object TwinsHelper {
private val firstCardSeries = listOf("CB61", "CB64")
private val secondCardSeries = listOf("CB62", "CB65")
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
@Suppress("MagicNumber")
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
fun getTwinCardNumber(cardId: String): TwinCardNumber? = when {
firstCardSeries.any(cardId::startsWith) -> TwinCardNumber.First
secondCardSeries.any(cardId::startsWith) -> TwinCardNumber.Second
else -> null
}
fun getTwinCardNumber(cardId: String): TwinCardNumber? = when {
firstCardSeries.any(cardId::startsWith) -> TwinCardNumber.First
secondCardSeries.any(cardId::startsWith) -> TwinCardNumber.Second
else -> null
}
fun getPairCardSeries(cardId: String): String? {
return when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val index = firstCardSeries.indexOf(cardId.take(4))
secondCardSeries[index]
}
TwinCardNumber.Second -> {
val index = secondCardSeries.indexOf(cardId.take(4))
firstCardSeries[index]
}
}
}
@Suppress("MagicNumber")
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
val twinCardId = cardId.substring(11..14)
val twinCardNumber = getTwinCardNumber(cardId)?.number ?: 1
return "$twinCardId #$twinCardNumber"
}
private val firstCardSeries = listOf("CB61", "CB64")
private val secondCardSeries = listOf("CB62", "CB65")
val twinCardId = cardId.substring(11..14)
val twinCardNumber = getTwinCardNumber(cardId)?.number ?: 1
return "$twinCardId #$twinCardNumber"
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.common.extensions
import com.tangem.blockchain.common.Blockchain
@Suppress("ComplexMethod")
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
return when (networkId) {
"arbitrum-one" -> Blockchain.Arbitrum
@ -52,6 +53,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
}
}
@Suppress("ComplexMethod")
fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
@ -103,6 +105,7 @@ fun Blockchain.toNetworkId(): String {
}
}
@Suppress("ComplexMethod")
fun Blockchain.toCoinId(): String {
return when (this) {
Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.BSC, Blockchain.BSCTestnet -> "binancecoin"

View file

@ -12,7 +12,7 @@ interface DataConverterVisitor<Data, Result> {
interface FieldDataConverter<Result> : DataConverterVisitor<FieldData, Result>
abstract class BaseFieldDataConverter<Result>() : FieldDataConverter<Result> {
abstract class BaseFieldDataConverter<Result> : FieldDataConverter<Result> {
protected val collectIds: List<FieldId>
get() = getIdToCollect()
@ -31,7 +31,7 @@ abstract class BaseFieldDataConverter<Result>() : FieldDataConverter<Result> {
class FieldToJsonConverter(
private val fieldsToConvert: List<FieldId> = listOf(),
protected val jsonConverter: MoshiJsonConverter
protected val jsonConverter: MoshiJsonConverter,
) : BaseFieldDataConverter<String>() {
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")

View file

@ -13,26 +13,34 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
abstract class CustomTokenValidator<T> : Validator<T, AddCustomTokenError>
interface CustomTokenValidator<T> : Validator<T, AddCustomTokenError>
class StringIsEmptyValidator : CustomTokenValidator<String>() {
class StringIsEmptyValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = when {
data == null || data.isEmpty() -> null
else -> AddCustomTokenError.FieldIsNotEmpty
}
}
class StringIsNotEmptyValidator : CustomTokenValidator<String>() {
class StringIsNotEmptyValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = when {
data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty
else -> null
}
}
class TokenContractAddressValidator : CustomTokenValidator<String>() {
class TokenContractAddressValidator : CustomTokenValidator<String> {
private var blockchain: Blockchain = Blockchain.Unknown
private val successAddressValidator = object : AddressService() {
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
throw UnsupportedOperationException()
}
override fun validate(address: String): Boolean = true
}
fun nextValidationFor(blockchain: Blockchain) {
this.blockchain = blockchain
}
@ -63,32 +71,25 @@ class TokenContractAddressValidator : CustomTokenValidator<String>() {
}
}
}
private val successAddressValidator = object : AddressService() {
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
throw UnsupportedOperationException()
}
override fun validate(address: String): Boolean = true
}
}
class TokenNetworkValidator : CustomTokenValidator<Blockchain>() {
class TokenNetworkValidator : CustomTokenValidator<Blockchain> {
override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) {
null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
else -> null
}
}
class TokenNameValidator : CustomTokenValidator<String>() {
class TokenNameValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
}
class TokenSymbolValidator : CustomTokenValidator<String>() {
class TokenSymbolValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
}
class TokenDecimalsValidator : CustomTokenValidator<String>() {
@Suppress("MagicNumber")
class TokenDecimalsValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? {
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty

View file

@ -25,6 +25,7 @@ class UserWalletId(
return stringValue.hashCode()
}
@Suppress("MagicNumber")
override fun toString(): String {
return with(stringValue) {
"UserWalletId(${take(3)}...${takeLast(3)})"

View file

@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.onEach
/**
[REDACTED_AUTHOR]
*/
@Suppress("MagicNumber")
class ValueDebouncer<T>(
private val initialValue: T,
private val debounceDuration: Long = 400,

View file

@ -48,7 +48,7 @@ class AddCustomTokenService(
return@withContext emptyList()
}
throw IllegalStateException("Unreachable code because runCatching must return result")
error("Unreachable code because runCatching must return result")
}
private fun selectNetworksForSearch(networkId: String?): String {

View file

@ -18,11 +18,11 @@ sealed class CustomCurrency(
class CustomBlockchain(
network: Blockchain,
derivationPath: DerivationPath?
derivationPath: DerivationPath?,
) : CustomCurrency(network, derivationPath) {
class Converter(
private val derivationStyle: DerivationStyle?
private val derivationStyle: DerivationStyle?,
) : BaseFieldDataConverter<CustomBlockchain>() {
override fun getConvertedData(): CustomBlockchain {
val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain
@ -30,24 +30,25 @@ sealed class CustomCurrency(
val derivationPath = AddCustomTokenState.getDerivationPath(
mainNetwork,
derivationPathNetwork,
derivationStyle
derivationStyle,
)
return CustomBlockchain(mainNetwork, derivationPath)
}
override fun getIdToCollect(): List<FieldId> = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
override fun getIdToCollect(): List<FieldId> =
listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
}
}
class CustomToken(
val token: Token,
network: Blockchain,
derivationPath: DerivationPath?
derivationPath: DerivationPath?,
) : CustomCurrency(network, derivationPath) {
class Converter(
private val tokenId: String?,
private val derivationStyle: DerivationStyle?
private val derivationStyle: DerivationStyle?,
) : BaseFieldDataConverter<CustomToken>() {
override fun getConvertedData(): CustomToken {
@ -56,7 +57,7 @@ sealed class CustomCurrency(
val derivationPath = AddCustomTokenState.getDerivationPath(
mainNetwork,
derivationPathNetwork,
derivationStyle
derivationStyle,
)
val token = Token(

View file

@ -31,7 +31,10 @@ sealed class AddCustomTokenAction : Action {
data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data<Blockchain>) : AddCustomTokenAction()
data class OnTokenNameChanged(val tokenName: Field.Data<String>) : AddCustomTokenAction()
data class OnTokenSymbolChanged(val tokenSymbol: Field.Data<String>) : AddCustomTokenAction()
data class OnTokenDerivationPathChanged(val blockchainDerivationPath: Field.Data<Blockchain>) : AddCustomTokenAction()
data class OnTokenDerivationPathChanged(
val blockchainDerivationPath: Field.Data<Blockchain>,
) : AddCustomTokenAction()
data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data<String>) : AddCustomTokenAction()
object OnAddCustomTokenClicked : AddCustomTokenAction()
@ -51,7 +54,7 @@ sealed class AddCustomTokenAction : Action {
data class Remove(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
data class Replace(
val remove: Set<AddCustomTokenError.Warning>,
val add: Set<AddCustomTokenError.Warning>
val add: Set<AddCustomTokenError.Warning>,
) : Warning()
}

View file

@ -66,6 +66,7 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomTokenHub") {
private val hubState: AddCustomTokenState
@ -79,10 +80,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
return storeState.copy(addCustomTokensState = newHubState)
}
@Suppress("ComplexMethod")
override suspend fun handleAction(
action: Action,
storeState: DomainState,
cancel: ValueCallback<Action>
cancel: ValueCallback<Action>,
) {
if (action !is AddCustomTokenAction) return
@ -225,7 +227,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
val derivationField = DerivationPath.getField<TokenDerivationPathField>()
derivationField.data = derivationField.data.copy(
value = Blockchain.Unknown,
isUserInput = false
isUserInput = false,
)
state.setField(derivationField)
dispatchOnMain(UpdateForm(hubState))
@ -344,6 +346,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
}
}
@Suppress("MagicNumber")
private suspend fun requestInfoAboutToken(contractAddress: String): List<CoinsResponse.Coin> {
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
@ -375,7 +378,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
tokenId: String? = hubState.foundToken?.id,
tokenContractAddress: String = ContractAddress.getFieldValue(),
tokenNetworkId: String = Network.getFieldValue<Blockchain>().toNetworkId(),
selectedDerivation: Blockchain = DerivationPath.getFieldValue()
selectedDerivation: Blockchain = DerivationPath.getFieldValue(),
): Boolean {
val savedCurrencies = hubState.appSavedCurrencies ?: return false
@ -388,6 +391,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress
val sameBlockchain = Blockchain.fromNetworkId(tokenNetworkId) == wrappedCurrency.blockchain
val sameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath
@Suppress("ComplexCondition")
if (sameId && sameAddress && sameBlockchain && sameDerivationPath) {
return true
}
@ -399,7 +403,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private fun isBlockchainPersistIntoAppSavedTokensList(
selectedNetwork: Blockchain = Network.getFieldValue(),
selectedDerivation: Blockchain = DerivationPath.getFieldValue()
selectedDerivation: Blockchain = DerivationPath.getFieldValue(),
): Boolean {
val state = hubState
val savedCurrencies = state.appSavedCurrencies ?: return false
@ -419,11 +423,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
}
private fun getDerivationPathFromSelectedBlockchain(
selectedDerivationBlockchain: Blockchain
selectedDerivationBlockchain: Blockchain,
): com.tangem.common.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath(
mainNetwork = Network.getFieldValue(),
derivationNetwork = selectedDerivationBlockchain,
derivationStyle = hubState.cardDerivationStyle
derivationStyle = hubState.cardDerivationStyle,
)
private suspend fun fillTokenFields(token: CoinsResponse.Coin, coinNetwork: CoinsResponse.Coin.Network) {
@ -482,7 +486,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw AddCustomTokenError.UnAppropriateInitialization(
"AddCustomTokenHub", "$objName must be not NULL"
"AddCustomTokenHub", "$objName must be not NULL",
)
}
@ -567,16 +571,14 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private suspend fun AddCustomTokenError.Warning.remove() {
dispatchOnMain(Warning.Remove(setOf(this)))
}
private suspend fun AddCustomTokenError.Warning.replaceBy(to: AddCustomTokenError.Warning) {
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
}
}
@Suppress("ComplexMethod")
private class AddCustomTokenReducer(
private val globalState: DomainGlobalState,
) : ReStoreReducer<AddCustomTokenState> {
@Suppress("LongMethod")
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
return when (action) {
is Init.SetAddedCurrencies -> {

View file

@ -265,7 +265,7 @@ data class AddCustomTokenState(
private var builder: StringBuilder = StringBuilder()
override fun convert(action: Action, stateHolder: DomainState): String? {
val action = (action as? AddCustomTokenAction) ?: return null
val action = action as? AddCustomTokenAction ?: return null
val state = stateHolder.addCustomTokensState
val fieldConverter =

View file

@ -9,8 +9,6 @@ import org.rekotlin.Store
/**
[REDACTED_AUTHOR]
*/
private class DomainStore // for simple search
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
DomainGlobalHub(),
AddCustomTokenHub(),
@ -19,7 +17,7 @@ private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
val domainStore = Store(
state = DomainState(),
middleware = RE_STORE_HUBS.map { it.getMiddleware() },
reducer = { action, state -> reduce(action, state) }
reducer = { action, state -> reduce(action, state) },
)
private fun reduce(action: Action, domainState: DomainState?): DomainState {