Skip to content

Commit

Permalink
Fix price alerts
Browse files Browse the repository at this point in the history
  • Loading branch information
furenster committed Oct 28, 2024
1 parent 145985e commit eb34a87
Show file tree
Hide file tree
Showing 17 changed files with 1,241 additions and 102 deletions.
955 changes: 955 additions & 0 deletions app/schemas/com.gemwallet.android.data.database.GemDatabase/34.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.wallet.core.primitives.AssetPricesRequest
import com.wallet.core.primitives.AssetSubtype
import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.Currency
import com.wallet.core.primitives.PriceAlert
import com.wallet.core.primitives.TransactionExtended
import com.wallet.core.primitives.Wallet
import com.wallet.core.primitives.WalletType
Expand Down Expand Up @@ -76,7 +75,6 @@ class AssetsRepository @Inject constructor(
sessionRepository.session().collectLatest {
sync(it?.currency ?: return@collectLatest)
}
updatePriceAlerts()
}
}

Expand Down Expand Up @@ -275,43 +273,6 @@ class AssetsRepository @Inject constructor(
?.awaitAll()
}

suspend fun includeAssetAlert(assetId: AssetId) = withContext(Dispatchers.IO) {
try {
gemApi.includePriceAlert(configRepository.getDeviceId(), listOf(PriceAlert(assetId.toIdentifier())))
} catch (err: Throwable) {
Log.d("PRICE_ALERT", "Include error:", err)
}
configRepository.includePriceAlert(assetId)
}

suspend fun excludeAssetAlert(assetId: AssetId) = withContext(Dispatchers.IO) {
try {
gemApi.excludePriceAlert(configRepository.getDeviceId(), listOf(PriceAlert(assetId.toIdentifier())))
} catch (err: Throwable) {
Log.d("PRICE_ALERT", "Exclude error:", err)
}
configRepository.excludePriceAlert(assetId)
}

suspend fun getPriceAlerts(): List<PriceAlert> {
return configRepository.getPriceAlerts()
}

suspend fun updatePriceAlerts() = withContext(Dispatchers.IO) {
val local = configRepository.getPriceAlerts()
val alerts = gemApi.getPriceAlerts(configRepository.getDeviceId()).getOrNull() ?: emptyList()

val notExcluded = alerts.filter { remote -> local.firstOrNull { it.assetId == remote.assetId } == null }
val notIncluded = local.filter { remote -> alerts.firstOrNull { it.assetId == remote.assetId } == null }
if (notExcluded.isNotEmpty()) {
gemApi.excludePriceAlert(configRepository.getDeviceId(), notExcluded)
}
if (notIncluded.isNotEmpty()) {
gemApi.includePriceAlert(configRepository.getDeviceId(), notIncluded)
}
configRepository.setPriceAlerts(alerts)
}

private suspend fun updateBalances(account: Account, tokens: List<AssetId>): List<Balances> {
val balances = balancesRemoteSource.getBalances(account, tokens)
assetsLocalSource.setBalances(account, balances)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ interface ConfigRepository {

fun increaseLaunchNumber()

fun getPriceAlerts(): List<PriceAlert>

fun excludePriceAlert(assetId: AssetId)

fun includePriceAlert(assetId: AssetId)

fun setPriceAlerts(alerts: List<PriceAlert>)

fun setEnablePriceAlerts(enabled: Boolean)

fun isPriceAlertEnabled(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ package com.gemwallet.android.data.config
import android.content.Context
import android.content.SharedPreferences
import com.gemwallet.android.BuildConfig
import com.gemwallet.android.ext.toIdentifier
import com.gemwallet.android.serializer.AssetIdSerializer
import com.gemwallet.android.services.GemApiClient
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.wallet.core.primitives.AssetId
import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.FiatAssets
import com.wallet.core.primitives.Node
import com.wallet.core.primitives.PriceAlert
import uniffi.Gemstone.Config
import java.util.UUID

Expand Down Expand Up @@ -172,29 +169,6 @@ class OfflineFirstConfigRepository(
putInt(Keys.LaunchNumber, getInt(Keys.LaunchNumber) + 1)
}

override fun getPriceAlerts(): List<PriceAlert> {
val data = getString(Keys.PriceAlerts)
val priceAlertsType = object : TypeToken<List<PriceAlert>>() {}.type
val priceAlerts = gson.fromJson<List<PriceAlert>>(data, priceAlertsType) ?: emptyList()
return priceAlerts
}

override fun excludePriceAlert(assetId: AssetId) {
val priceAlerts = getPriceAlerts()
setPriceAlerts(priceAlerts.filter { it.assetId != assetId.toIdentifier() })
}

override fun includePriceAlert(assetId: AssetId) {
val priceAlerts = getPriceAlerts().toMutableList()
priceAlerts.add(PriceAlert(assetId = assetId.toIdentifier()))
setPriceAlerts(priceAlerts)
}

override fun setPriceAlerts(alerts: List<PriceAlert>) {
val data = gson.toJson(alerts)
putString(Keys.PriceAlerts, data)
}

override fun setEnablePriceAlerts(enabled: Boolean) {
putBoolean(Keys.PriceAlertsEnabled, enabled)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.gemwallet.android.data.database.entities.DbAssetInfo
import com.gemwallet.android.data.database.entities.DbBalance
import com.gemwallet.android.data.database.entities.DbBanner
import com.gemwallet.android.data.database.entities.DbPrice
import com.gemwallet.android.data.database.entities.DbPriceAlert
import com.gemwallet.android.data.database.entities.DbSession
import com.gemwallet.android.data.database.entities.DbToken
import com.gemwallet.android.data.database.entities.DbTransaction
Expand All @@ -20,14 +21,13 @@ import com.gemwallet.android.data.database.entities.DbTxSwapMetadata
import com.gemwallet.android.data.stake.RoomDelegationBase
import com.gemwallet.android.data.stake.RoomDelegationValidator
import com.gemwallet.android.data.stake.StakeDao
import com.gemwallet.android.data.database.TokensDao
import com.gemwallet.android.data.wallet.AccountRoom
import com.gemwallet.android.data.wallet.AccountsDao
import com.gemwallet.android.data.wallet.WalletRoom
import com.gemwallet.android.data.wallet.WalletsDao

@Database(
version = 33,
version = 34,
entities = [
WalletRoom::class,
AccountRoom::class,
Expand All @@ -44,6 +44,7 @@ import com.gemwallet.android.data.wallet.WalletsDao
DbSession::class,
DbAssetConfig::class,
DbBanner::class,
DbPriceAlert::class,
],
views = [
DbAssetInfo::class,
Expand Down Expand Up @@ -74,4 +75,6 @@ abstract class GemDatabase : RoomDatabase() {
abstract fun sessionDao(): SessionDao

abstract fun bannersDao(): BannersDao

abstract fun priceAlertsDao(): PriceAlertsDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.gemwallet.android.data.database

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.gemwallet.android.data.database.entities.DbPriceAlert
import kotlinx.coroutines.flow.Flow

@Dao
interface PriceAlertsDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun put(alerts: List<DbPriceAlert>)

@Query(
"""
SELECT * FROM price_alerts WHERE enabled = 1
"""
)
fun getAlerts(): Flow<List<DbPriceAlert>>

@Query(
"""
SELECT * FROM price_alerts WHERE asset_id = :assetId
"""
)
fun getAlert(assetId: String): Flow<DbPriceAlert?>

@Query(
"""
UPDATE price_alerts SET enabled = :enabled WHERE asset_id = :assetId
"""
)
suspend fun enabled(assetId: String, enabled: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import com.gemwallet.android.data.database.AssetsDao
import com.gemwallet.android.data.database.BalancesDao
import com.gemwallet.android.data.database.BannersDao
import com.gemwallet.android.data.database.GemDatabase
import com.gemwallet.android.data.database.PriceAlertsDao
import com.gemwallet.android.data.database.PricesDao
import com.gemwallet.android.data.database.SessionDao
import com.gemwallet.android.data.database.TokensDao
import com.gemwallet.android.data.database.TransactionsDao
import com.gemwallet.android.data.database.entities.SESSION_REQUEST
import com.gemwallet.android.data.repositories.session.SessionSharedPreferenceSource
import com.gemwallet.android.data.stake.StakeDao
import com.gemwallet.android.data.database.TokensDao
import com.gemwallet.android.data.wallet.AccountsDao
import com.gemwallet.android.data.wallet.WalletsDao
import com.wallet.core.primitives.Chain
Expand Down Expand Up @@ -71,6 +72,7 @@ object DatabaseModule {
.addMigrations(MIGRATION_30_31)
.addMigrations(MIGRATION_31_32)
.addMigrations(MIGRATION_32_33)
.addMigrations(MIGRATION_33_34)
.build()

@Singleton
Expand Down Expand Up @@ -120,6 +122,10 @@ object DatabaseModule {
@Singleton
@Provides
fun provideBannersDao(db: GemDatabase): BannersDao = db.bannersDao()

@Singleton
@Provides
fun providePriceAlertsDao(db: GemDatabase): PriceAlertsDao = db.priceAlertsDao()
}

val MIGRATION_1_2 = object : Migration(1, 2) {
Expand Down Expand Up @@ -714,4 +720,18 @@ val MIGRATION_32_33 = object : Migration(32, 33) {
| GROUP BY tx.id
""".trimMargin())
}
}

val MIGRATION_33_34 = object : Migration(33, 34) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("""
CREATE TABLE price_alerts (
asset_id TEXT NOT NULL,
enabled INTEGER NOT NULL,
price REAL DEFAULT NULL,
price_percent_change REAL DEFAULT NULL,
price_direction TEXT DEFAULT NULL,
PRIMARY KEY (asset_id)
)""".trimIndent())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gemwallet.android.data.database.entities

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.wallet.core.primitives.PriceAlertDirection

@Entity(tableName = "price_alerts")
data class DbPriceAlert(
@PrimaryKey @ColumnInfo("asset_id") val assetId: String,
val price: Double? = null,
@ColumnInfo("price_percent_change")
val pricePercentChange: Double? = null,
@ColumnInfo("price_direction")
val priceDirection: PriceAlertDirection? = null,
val enabled: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.gemwallet.android.data.database.mappers

import com.gemwallet.android.data.database.entities.DbPriceAlert
import com.wallet.core.primitives.PriceAlert

class PriceAlertMapper : Mapper<PriceAlert, DbPriceAlert> {
override fun asDomain(entity: PriceAlert): DbPriceAlert {
return DbPriceAlert(
assetId = entity.assetId,
price = entity.price,
pricePercentChange = entity.pricePercentChange,
priceDirection = entity.priceDirection,
enabled = true,
)
}

override fun asEntity(domain: DbPriceAlert): PriceAlert {
return PriceAlert(
assetId = domain.assetId,
price = domain.price,
priceDirection = domain.priceDirection,
pricePercentChange = domain.pricePercentChange,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.gemwallet.android.data.repositories.pricealerts

import com.gemwallet.android.cases.pricealerts.EnablePriceAlertCase
import com.gemwallet.android.cases.pricealerts.GetPriceAlertsCase
import com.gemwallet.android.cases.pricealerts.PutPriceAlertCase
import com.gemwallet.android.data.config.ConfigRepository
import com.gemwallet.android.data.database.PriceAlertsDao
import com.gemwallet.android.data.database.entities.DbPriceAlert
import com.gemwallet.android.data.database.mappers.PriceAlertMapper
import com.gemwallet.android.ext.toIdentifier
import com.gemwallet.android.services.GemApiClient
import com.wallet.core.primitives.AssetId
import com.wallet.core.primitives.PriceAlert
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.collections.map

class PriceAlertRepository(
private val gemClient: GemApiClient,
private val priceAlertsDao: PriceAlertsDao,
private val configRepository: ConfigRepository,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) : GetPriceAlertsCase, PutPriceAlertCase, EnablePriceAlertCase {

private val mapper = PriceAlertMapper()

init {
scope.launch(Dispatchers.IO) { sync() }
}

override fun getPriceAlerts(): Flow<List<PriceAlert>> {
return priceAlertsDao.getAlerts().map { items -> items.map(mapper::asEntity) }
}

override fun getPriceAlert(assetId: AssetId): Flow<PriceAlert?> {
return priceAlertsDao.getAlert(assetId.toIdentifier()).filterNotNull().map(mapper::asEntity)
}

override suspend fun enabled(assetId: AssetId, enabled: Boolean): Unit = withContext(Dispatchers.IO) {
val assetIdentifier = assetId.toIdentifier()
val priceAlert = priceAlertsDao.getAlert(assetIdentifier).firstOrNull().let {
if (it == null) {
priceAlertsDao.put(listOf(DbPriceAlert(assetIdentifier, enabled = true)))
listOf(PriceAlert(assetIdentifier))
} else {
listOf(mapper.asEntity(it))
}
}
priceAlertsDao.enabled(assetIdentifier, enabled)

launch(Dispatchers.IO) {
if (enabled) {
gemClient.includePriceAlert(getDeviceId(), priceAlert)
} else {
gemClient.excludePriceAlert(getDeviceId(), priceAlert)
}
}
}

override suspend fun putPriceAlert(alert: PriceAlert) = withContext(Dispatchers.IO) {
priceAlertsDao.put(listOf(mapper.asDomain(alert)))
launch(Dispatchers.IO) {
gemClient.includePriceAlert(getDeviceId(), listOf(alert))
}
Unit
}

override fun enabled(assetId: AssetId): Flow<Boolean> {
return priceAlertsDao.getAlert(assetId.toIdentifier()).map { it != null && it.enabled }
}

private suspend fun sync() {
gemClient.includePriceAlert(getDeviceId(), getPriceAlerts().firstOrNull() ?: emptyList())
val local = priceAlertsDao.getAlerts().firstOrNull() ?: emptyList()
val remote = gemClient.getPriceAlerts(getDeviceId()).getOrNull() ?: return
val toExclude = remote.filter { remote ->
local.firstOrNull { it.assetId == remote.assetId } == null
}
gemClient.excludePriceAlert(getDeviceId(), toExclude)
}

private fun getDeviceId() = configRepository.getDeviceId()
}
Loading

0 comments on commit eb34a87

Please sign in to comment.