From 52cb5d4adfbb6695e6a1370d3f61d1fed2aed631 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 May 2026 09:21:30 +0400 Subject: [PATCH] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 1 + .../1.json | 439 ++++++++++++++++++ .../tangem/datasource/di/TxHistoryModule.kt | 72 +++ .../local/txhistory/db/TxHistoryDatabase.kt | 21 + .../txhistory/db/entity/ExpressHistoryDao.kt | 87 ++++ .../entity/express/ExpressExchangeEntity.kt | 130 ++++++ .../db/entity/express/ExpressOnrampEntity.kt | 122 +++++ .../entity/express/ExpressProviderEntity.kt | 24 + .../txhistory/store/DefaultTxHistoryStore.kt | 38 ++ .../local/txhistory/store/SyncStateModel.kt | 27 ++ .../local/txhistory/store/TxHistoryStore.kt | 12 + .../utils/KotlinxDataStoreSerializer.kt | 56 +++ 12 files changed, 1029 insertions(+) create mode 100644 core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/TxHistoryDatabase.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/ExpressHistoryDao.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/DefaultTxHistoryStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/SyncStateModel.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/TxHistoryStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/KotlinxDataStoreSerializer.kt diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 34a72ebb46..028d38c5ec 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -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) diff --git a/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json b/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json new file mode 100644 index 0000000000..dd3a028439 --- /dev/null +++ b/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryModule.kt new file mode 100644 index 0000000000..eb42fb43f2 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryModule.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/TxHistoryDatabase.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/TxHistoryDatabase.kt new file mode 100644 index 0000000000..6cf81922b3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/TxHistoryDatabase.kt @@ -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 +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/ExpressHistoryDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/ExpressHistoryDao.kt new file mode 100644 index 0000000000..4d38379239 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/ExpressHistoryDao.kt @@ -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) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertExchanges(items: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertOnramps(items: List) + + @Query( + """ + SELECT * + FROM express_exchange + WHERE owner_address = :ownerAddress + ORDER BY updated_at DESC + """, + ) + fun observeExchanges(ownerAddress: String): Flow> + + @Query( + """ + SELECT * + FROM express_onramp + WHERE owner_address = :ownerAddress + ORDER BY updated_at DESC + """, + ) + fun observeOnramps(ownerAddress: String): Flow> + + @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? +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt new file mode 100644 index 0000000000..d0d7cc7c87 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt @@ -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?, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt new file mode 100644 index 0000000000..3c416ff881 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt @@ -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?, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt new file mode 100644 index 0000000000..55c8458134 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/DefaultTxHistoryStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/DefaultTxHistoryStore.kt new file mode 100644 index 0000000000..e444e0b7a2 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/DefaultTxHistoryStore.kt @@ -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>, + private val expressOnrampStore: DataStore>, +) : TxHistoryStore { + + override fun expressExchangeSyncState(key: CommonSyncStateKey): Flow { + return expressExchangeStore.data.map { map -> map.getOrDefault(key) } + } + + override fun expressOnrampSyncState(key: CommonSyncStateKey): Flow { + 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.getOrDefault(key: CommonSyncStateKey): CommonSyncState = + this.getOrDefault(key, CommonSyncState.default(key)) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/SyncStateModel.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/SyncStateModel.kt new file mode 100644 index 0000000000..38e571202a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/SyncStateModel.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/TxHistoryStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/TxHistoryStore.kt new file mode 100644 index 0000000000..08f7b32a26 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/store/TxHistoryStore.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local.txhistory.store + +import kotlinx.coroutines.flow.Flow + +interface TxHistoryStore { + + fun expressExchangeSyncState(key: CommonSyncStateKey): Flow + fun expressOnrampSyncState(key: CommonSyncStateKey): Flow + + suspend fun updateExpressExchangeSyncState(key: CommonSyncStateKey, value: CommonSyncState): CommonSyncState + suspend fun updateExpressOnrampSyncState(key: CommonSyncStateKey, value: CommonSyncState): CommonSyncState +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/KotlinxDataStoreSerializer.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/KotlinxDataStoreSerializer.kt new file mode 100644 index 0000000000..86e44d431a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/KotlinxDataStoreSerializer.kt @@ -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( + override val defaultValue: T, + private val serializer: KSerializer, + private val json: Json = DefaultJson, +) : Serializer { + + 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) + } + } +} \ No newline at end of file