Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-22 09:21:30 +04:00
parent 3531c95908
commit 52cb5d4adf
12 changed files with 1029 additions and 0 deletions

View file

@ -6,6 +6,7 @@ plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
alias(deps.plugins.room)
alias(deps.plugins.ksp)

View file

@ -0,0 +1,439 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "aafa8b51b5a5a32d0ec2b0720cec6c1e",
"entities": [
{
"tableName": "express_provider",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `icon_url` TEXT NOT NULL, `provider_url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconUrl",
"columnName": "icon_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "providerUrl",
"columnName": "provider_url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "express_exchange",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `status` TEXT NOT NULL, `to_is_actual` INTEGER NOT NULL DEFAULT 0, `payin_hash` TEXT, `payout_hash` TEXT, `external_tx_id` TEXT, `external_tx_url` TEXT, `rate_type` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `from_network` TEXT NOT NULL, `from_token_id` TEXT, `from_raw_amount` TEXT NOT NULL, `from_decimals` INTEGER NOT NULL, `to_network` TEXT NOT NULL, `to_token_id` TEXT, `to_raw_amount` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `refund_network` TEXT, `refund_token_id` TEXT, `refund_raw_amount` TEXT, `refund_decimals` INTEGER, `refund_hash` TEXT, PRIMARY KEY(`tx_id`), FOREIGN KEY(`provider_id`) REFERENCES `express_provider`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
"fields": [
{
"fieldPath": "txId",
"columnName": "tx_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ownerAddress",
"columnName": "owner_address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "providerId",
"columnName": "provider_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "toIsActual",
"columnName": "to_is_actual",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "payinHash",
"columnName": "payin_hash",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "payoutHash",
"columnName": "payout_hash",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "externalTxId",
"columnName": "external_tx_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "externalTxUrl",
"columnName": "external_tx_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rateType",
"columnName": "rate_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updated_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "from.network",
"columnName": "from_network",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "from.tokenId",
"columnName": "from_token_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from.rawAmount",
"columnName": "from_raw_amount",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "from.decimals",
"columnName": "from_decimals",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "to.network",
"columnName": "to_network",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "to.tokenId",
"columnName": "to_token_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to.rawAmount",
"columnName": "to_raw_amount",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "to.decimals",
"columnName": "to_decimals",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "refund.network",
"columnName": "refund_network",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "refund.tokenId",
"columnName": "refund_token_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "refund.rawAmount",
"columnName": "refund_raw_amount",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "refund.decimals",
"columnName": "refund_decimals",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "refund.hash",
"columnName": "refund_hash",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"tx_id"
]
},
"indices": [
{
"name": "index_express_exchange_owner_address_from_network_updated_at",
"unique": false,
"columnNames": [
"owner_address",
"from_network",
"updated_at"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_from_network_updated_at` ON `${TABLE_NAME}` (`owner_address`, `from_network`, `updated_at`)"
},
{
"name": "index_express_exchange_owner_address_payin_hash",
"unique": false,
"columnNames": [
"owner_address",
"payin_hash"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_payin_hash` ON `${TABLE_NAME}` (`owner_address`, `payin_hash`)"
},
{
"name": "index_express_exchange_owner_address_payout_hash",
"unique": false,
"columnNames": [
"owner_address",
"payout_hash"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_payout_hash` ON `${TABLE_NAME}` (`owner_address`, `payout_hash`)"
},
{
"name": "index_express_exchange_owner_address_refund_hash",
"unique": false,
"columnNames": [
"owner_address",
"refund_hash"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_refund_hash` ON `${TABLE_NAME}` (`owner_address`, `refund_hash`)"
}
],
"foreignKeys": [
{
"table": "express_provider",
"onDelete": "RESTRICT",
"onUpdate": "NO ACTION",
"columns": [
"provider_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "express_onramp",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `status` TEXT NOT NULL, `from_currency_code` TEXT NOT NULL, `from_amount` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_token_id` TEXT, `to_expected_raw_amount` TEXT NOT NULL, `to_actual_raw_amount` TEXT, `to_decimals` INTEGER NOT NULL, `payout_hash` TEXT, `external_tx_id` TEXT, `external_tx_url` TEXT, `rate_type` TEXT NOT NULL, `fail_reason` TEXT, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `refund_currency_code` TEXT, `refund_amount` TEXT, PRIMARY KEY(`tx_id`), FOREIGN KEY(`provider_id`) REFERENCES `express_provider`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
"fields": [
{
"fieldPath": "txId",
"columnName": "tx_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ownerAddress",
"columnName": "owner_address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "providerId",
"columnName": "provider_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fromCurrencyCode",
"columnName": "from_currency_code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fromAmount",
"columnName": "from_amount",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "toNetwork",
"columnName": "to_network",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "toTokenId",
"columnName": "to_token_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "toExpectedRawAmount",
"columnName": "to_expected_raw_amount",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "toActualRawAmount",
"columnName": "to_actual_raw_amount",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "toDecimals",
"columnName": "to_decimals",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "payoutHash",
"columnName": "payout_hash",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "externalTxId",
"columnName": "external_tx_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "externalTxUrl",
"columnName": "external_tx_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rateType",
"columnName": "rate_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "failReason",
"columnName": "fail_reason",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updated_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "refund.currencyCode",
"columnName": "refund_currency_code",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "refund.amount",
"columnName": "refund_amount",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"tx_id"
]
},
"indices": [
{
"name": "index_express_onramp_owner_address_to_network_updated_at",
"unique": false,
"columnNames": [
"owner_address",
"to_network",
"updated_at"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_owner_address_to_network_updated_at` ON `${TABLE_NAME}` (`owner_address`, `to_network`, `updated_at`)"
},
{
"name": "index_express_onramp_owner_address_payout_hash",
"unique": false,
"columnNames": [
"owner_address",
"payout_hash"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_owner_address_payout_hash` ON `${TABLE_NAME}` (`owner_address`, `payout_hash`)"
}
],
"foreignKeys": [
{
"table": "express_provider",
"onDelete": "RESTRICT",
"onUpdate": "NO ACTION",
"columns": [
"provider_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aafa8b51b5a5a32d0ec2b0720cec6c1e')"
]
}
}

View file

@ -0,0 +1,72 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import androidx.room.Room
import com.tangem.datasource.local.txhistory.db.TxHistoryDatabase
import com.tangem.datasource.local.txhistory.store.CommonSyncState
import com.tangem.datasource.local.txhistory.store.CommonSyncStateKey
import com.tangem.datasource.local.txhistory.store.DefaultTxHistoryStore
import com.tangem.datasource.local.txhistory.store.TxHistoryStore
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
import com.tangem.datasource.utils.KotlinxDataStoreSerializer.Companion.jsonBuilder
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.builtins.MapSerializer
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface TxHistoryModule {
companion object {
private const val TX_HISTORY_DATABASE_NAME = "tx_history_database.db"
@Provides
@Singleton
fun provideTxHistoryDatabase(@ApplicationContext context: Context): TxHistoryDatabase {
return Room.databaseBuilder(
context = context,
klass = TxHistoryDatabase::class.java,
name = TX_HISTORY_DATABASE_NAME,
).build()
}
@Provides
@Singleton
fun provideTxHistoryStore(@ApplicationContext context: Context, appScope: AppCoroutineScope): TxHistoryStore {
val commonSerializer = KotlinxDataStoreSerializer(
defaultValue = emptyMap(),
serializer = MapSerializer(
CommonSyncStateKey.serializer(),
CommonSyncState.serializer(),
),
json = jsonBuilder {
allowStructuredMapKeys = true
},
)
val expressExchangeStore = DataStoreFactory.create(
serializer = commonSerializer,
produceFile = { context.dataStoreFile(fileName = "TxHistoryExpressExchangeStore") },
scope = appScope,
)
val expressOnrampStore = DataStoreFactory.create(
serializer = commonSerializer,
produceFile = { context.dataStoreFile(fileName = "TxHistoryExpressOnrampStore") },
scope = appScope,
)
return DefaultTxHistoryStore(
expressExchangeStore = expressExchangeStore,
expressOnrampStore = expressOnrampStore,
)
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.txhistory.db
import androidx.room.Database
import androidx.room.RoomDatabase
import com.tangem.datasource.local.txhistory.db.entity.ExpressHistoryDao
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
@Database(
version = 1,
entities = [
ExpressProviderEntity::class,
ExpressExchangeEntity::class,
ExpressOnrampEntity::class,
],
)
abstract class TxHistoryDatabase : RoomDatabase() {
abstract fun expressHistoryDao(): ExpressHistoryDao
}

View file

@ -0,0 +1,87 @@
package com.tangem.datasource.local.txhistory.db.entity
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface ExpressHistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertProviders(items: List<ExpressProviderEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertExchanges(items: List<ExpressExchangeEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertOnramps(items: List<ExpressOnrampEntity>)
@Query(
"""
SELECT *
FROM express_exchange
WHERE owner_address = :ownerAddress
ORDER BY updated_at DESC
""",
)
fun observeExchanges(ownerAddress: String): Flow<List<ExpressExchangeEntity>>
@Query(
"""
SELECT *
FROM express_onramp
WHERE owner_address = :ownerAddress
ORDER BY updated_at DESC
""",
)
fun observeOnramps(ownerAddress: String): Flow<List<ExpressOnrampEntity>>
@Query(
"""
SELECT *
FROM express_exchange
WHERE owner_address = :ownerAddress
AND payin_hash = :hash
LIMIT 1
""",
)
suspend fun findExchangeByPayinHash(ownerAddress: String, hash: String): ExpressExchangeEntity?
@Query(
"""
SELECT *
FROM express_exchange
WHERE owner_address = :ownerAddress
AND payout_hash = :hash
LIMIT 1
""",
)
suspend fun findExchangeByPayoutHash(ownerAddress: String, hash: String): ExpressExchangeEntity?
@Query(
"""
SELECT *
FROM express_exchange
WHERE owner_address = :ownerAddress
AND refund_hash = :hash
LIMIT 1
""",
)
suspend fun findExchangeByRefundHash(ownerAddress: String, hash: String): ExpressExchangeEntity?
@Query(
"""
SELECT *
FROM express_onramp
WHERE owner_address = :ownerAddress
AND payout_hash = :hash
LIMIT 1
""",
)
suspend fun findOnrampByPayoutHash(ownerAddress: String, hash: String): ExpressOnrampEntity?
}

View file

@ -0,0 +1,130 @@
package com.tangem.datasource.local.txhistory.db.entity.express
import androidx.room.*
@Suppress("BooleanPropertyNaming")
@Entity(
tableName = "express_exchange",
foreignKeys = [
ForeignKey(
entity = ExpressProviderEntity::class,
parentColumns = ["id"],
childColumns = ["provider_id"],
onDelete = ForeignKey.RESTRICT,
),
],
indices = [
Index(value = ["owner_address", "from_network", "updated_at"]),
Index(value = ["owner_address", "payin_hash"]),
Index(value = ["owner_address", "payout_hash"]),
Index(value = ["owner_address", "refund_hash"]),
],
)
data class ExpressExchangeEntity(
@PrimaryKey
@ColumnInfo(name = "tx_id")
val txId: String,
@ColumnInfo(name = "owner_address")
val ownerAddress: String,
@ColumnInfo(name = "provider_id")
val providerId: String,
/**
* waiting
* confirming
* exchanging
* sending
* finished
* failed
* refunded
* expired
*/
@ColumnInfo(name = "status")
val status: String,
@Embedded(prefix = "from_")
val from: AssetEmbedded,
@Embedded(prefix = "to_")
val to: AssetEmbedded,
/**
* true -> actual provider-confirmed amount
* false -> estimated amount
*/
@ColumnInfo(name = "to_is_actual", defaultValue = "0")
val toIsActual: Boolean,
/**
* Match key for PAYIN leg
*/
@ColumnInfo(name = "payin_hash")
val payinHash: String?,
/**
* Match key for PAYOUT leg
*/
@ColumnInfo(name = "payout_hash")
val payoutHash: String?,
@ColumnInfo(name = "external_tx_id")
val externalTxId: String?,
@ColumnInfo(name = "external_tx_url")
val externalTxUrl: String?,
/**
* fixed / float
*/
@ColumnInfo(name = "rate_type")
val rateType: String,
@ColumnInfo(name = "created_at")
val createdAt: Long,
@ColumnInfo(name = "updated_at")
val updatedAt: Long,
@Embedded(prefix = "refund_")
val refund: RefundEmbedded?,
) {
data class AssetEmbedded(
@ColumnInfo(name = "network")
val network: String,
@ColumnInfo(name = "token_id")
val tokenId: String?,
@ColumnInfo(name = "raw_amount")
val rawAmount: String,
@ColumnInfo(name = "decimals")
val decimals: Int,
)
data class RefundEmbedded(
@ColumnInfo(name = "network")
val network: String?,
@ColumnInfo(name = "token_id")
val tokenId: String?,
@ColumnInfo(name = "raw_amount")
val rawAmount: String?,
@ColumnInfo(name = "decimals")
val decimals: Int?,
/**
* Match key for REFUND leg
*/
@ColumnInfo(name = "hash")
val hash: String?,
)
}

View file

@ -0,0 +1,122 @@
package com.tangem.datasource.local.txhistory.db.entity.express
import androidx.room.*
@Entity(
tableName = "express_onramp",
foreignKeys = [
ForeignKey(
entity = ExpressProviderEntity::class,
parentColumns = ["id"],
childColumns = ["provider_id"],
onDelete = ForeignKey.RESTRICT,
),
],
indices = [
Index(value = ["owner_address", "to_network", "updated_at"]),
Index(value = ["owner_address", "payout_hash"]),
],
)
data class ExpressOnrampEntity(
@PrimaryKey
@ColumnInfo(name = "tx_id")
val txId: String,
@ColumnInfo(name = "owner_address")
val ownerAddress: String,
@ColumnInfo(name = "provider_id")
val providerId: String,
/**
* waiting-for-payment
* payment-processing
* paused
* verifying
* sending
* finished
* failed
* expired
* refunded
*/
@ColumnInfo(name = "status")
val status: String,
/**
* ISO-4217
*/
@ColumnInfo(name = "from_currency_code")
val fromCurrencyCode: String,
/**
* Decimal string
*/
@ColumnInfo(name = "from_amount")
val fromAmount: String,
@ColumnInfo(name = "to_network")
val toNetwork: String,
@ColumnInfo(name = "to_token_id")
val toTokenId: String?,
/**
* Estimated amount at creation moment
*/
@ColumnInfo(name = "to_expected_raw_amount")
val toExpectedRawAmount: String,
/**
* Actual provider-confirmed amount
*/
@ColumnInfo(name = "to_actual_raw_amount")
val toActualRawAmount: String?,
@ColumnInfo(name = "to_decimals")
val toDecimals: Int,
/**
* Match key with gateway_tx.hash
*/
@ColumnInfo(name = "payout_hash")
val payoutHash: String?,
@ColumnInfo(name = "external_tx_id")
val externalTxId: String?,
@ColumnInfo(name = "external_tx_url")
val externalTxUrl: String?,
/**
* fixed / float
*/
@ColumnInfo(name = "rate_type")
val rateType: String,
@ColumnInfo(name = "fail_reason")
val failReason: String?,
@ColumnInfo(name = "created_at")
val createdAt: Long,
@ColumnInfo(name = "updated_at")
val updatedAt: Long,
@Embedded(prefix = "refund_")
val refund: RefundEmbedded?,
) {
data class RefundEmbedded(
/**
* ISO-4217
*/
@ColumnInfo(name = "currency_code")
val currencyCode: String?,
@ColumnInfo(name = "amount")
val amount: String?,
)
}

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.local.txhistory.db.entity.express
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(
tableName = "express_provider",
)
data class ExpressProviderEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "name")
val name: String,
@ColumnInfo(name = "icon_url")
val iconUrl: String,
@ColumnInfo(name = "provider_url")
val providerUrl: String,
)

View file

@ -0,0 +1,38 @@
package com.tangem.datasource.local.txhistory.store
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class DefaultTxHistoryStore(
private val expressExchangeStore: DataStore<Map<CommonSyncStateKey, CommonSyncState>>,
private val expressOnrampStore: DataStore<Map<CommonSyncStateKey, CommonSyncState>>,
) : TxHistoryStore {
override fun expressExchangeSyncState(key: CommonSyncStateKey): Flow<CommonSyncState> {
return expressExchangeStore.data.map { map -> map.getOrDefault(key) }
}
override fun expressOnrampSyncState(key: CommonSyncStateKey): Flow<CommonSyncState> {
return expressOnrampStore.data.map { map -> map.getOrDefault(key) }
}
override suspend fun updateExpressExchangeSyncState(
key: CommonSyncStateKey,
value: CommonSyncState,
): CommonSyncState {
return expressExchangeStore.updateData { map -> map.plus(key to value) }
.getOrDefault(key)
}
override suspend fun updateExpressOnrampSyncState(
key: CommonSyncStateKey,
value: CommonSyncState,
): CommonSyncState {
return expressOnrampStore.updateData { map -> map.plus(key to value) }
.getOrDefault(key)
}
private fun Map<CommonSyncStateKey, CommonSyncState>.getOrDefault(key: CommonSyncStateKey): CommonSyncState =
this.getOrDefault(key, CommonSyncState.default(key))
}

View file

@ -0,0 +1,27 @@
package com.tangem.datasource.local.txhistory.store
import com.tangem.domain.models.account.AccountId
import kotlinx.serialization.Serializable
@Serializable
data class CommonSyncStateKey(
val accountId: AccountId,
val address: String,
)
@Serializable
data class CommonSyncState(
val accountId: AccountId,
val address: String,
val isInitialCompleted: Boolean,
val cursor: String?,
) {
companion object {
fun default(key: CommonSyncStateKey) = CommonSyncState(
accountId = key.accountId,
address = key.address,
isInitialCompleted = false,
cursor = null,
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.txhistory.store
import kotlinx.coroutines.flow.Flow
interface TxHistoryStore {
fun expressExchangeSyncState(key: CommonSyncStateKey): Flow<CommonSyncState>
fun expressOnrampSyncState(key: CommonSyncStateKey): Flow<CommonSyncState>
suspend fun updateExpressExchangeSyncState(key: CommonSyncStateKey, value: CommonSyncState): CommonSyncState
suspend fun updateExpressOnrampSyncState(key: CommonSyncStateKey, value: CommonSyncState): CommonSyncState
}

View file

@ -0,0 +1,56 @@
package com.tangem.datasource.utils
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonBuilder
import java.io.InputStream
import java.io.OutputStream
/**
* Kotlinx Serialization serializer for [androidx.datastore.core.DataStore]
*
*/
class KotlinxDataStoreSerializer<T>(
override val defaultValue: T,
private val serializer: KSerializer<T>,
private val json: Json = DefaultJson,
) : Serializer<T> {
override suspend fun readFrom(input: InputStream): T {
return try {
input.bufferedReader().use { reader ->
json.decodeFromString(
deserializer = serializer,
string = reader.readText(),
)
}
} catch (e: Exception) {
throw CorruptionException("Failed to deserialize data", e)
}
}
override suspend fun writeTo(t: T, output: OutputStream) {
output.bufferedWriter().use { writer ->
writer.write(
json.encodeToString(
serializer = serializer,
value = t,
),
)
}
}
companion object {
private val DefaultJson = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
fun jsonBuilder(builderAction: JsonBuilder.() -> Unit): Json {
return Json(DefaultJson, builderAction)
}
}
}