Updated on 2026-08-14
This commit is contained in:
parent
3531c95908
commit
52cb5d4adf
12 changed files with 1029 additions and 0 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue