Skip to content

Commit 50c2ba7

Browse files
committed
Refactor balance updates to preserve existing native rows
1 parent 0daad92 commit 50c2ba7

6 files changed

Lines changed: 259 additions & 104 deletions

File tree

android/blockchain/src/main/kotlin/com/gemwallet/android/blockchain/services/BalancesService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class BalancesService(
3939
}
4040
}
4141

42-
suspend fun getDelegationBalances(account: Account): AssetBalance? {
42+
suspend fun getStakeBalances(account: Account): AssetBalance? {
4343
return try {
4444
val result = gateway.getBalanceStaking(account.chain.string, account.address)
4545
?: return null

android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/assets/UpdateBalances.kt

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package com.gemwallet.android.data.repositories.assets
33
import com.gemwallet.android.blockchain.services.BalancesService
44
import com.gemwallet.android.data.service.store.database.BalancesDao
55
import com.gemwallet.android.data.service.store.database.entities.DbBalance
6-
import com.gemwallet.android.data.service.store.database.entities.mergeDelegation
7-
import com.gemwallet.android.data.service.store.database.entities.mergeNative
86
import com.gemwallet.android.data.service.store.database.entities.toDTO
9-
import com.gemwallet.android.data.service.store.database.entities.toRecord
7+
import com.gemwallet.android.ext.toIdentifier
108
import com.gemwallet.android.model.AssetBalance
119
import com.wallet.core.primitives.Account
1210
import com.wallet.core.primitives.Asset
@@ -22,52 +20,106 @@ class UpdateBalances(
2220
suspend fun updateBalances(
2321
walletId: String,
2422
account: Account,
25-
tokens: List<Asset>
23+
tokens: List<Asset>,
2624
): List<AssetBalance> = withContext(Dispatchers.IO) {
2725
val updatedAt = System.currentTimeMillis()
2826

29-
val getNative = async { updateNativeBalance(walletId, account, updatedAt) }
30-
31-
val getDelegation = async { balancesService.getDelegationBalances(account) }
32-
27+
val getNative = async { balancesService.getNativeBalances(account) }
28+
val getStake = async { balancesService.getStakeBalances(account) }
3329
val getTokens = async { updateTokensBalance(walletId, account, tokens, updatedAt) }
3430

3531
val native = getNative.await()
36-
val delegation = getDelegation.await()
37-
val fullNative = mergeNativeBalances(native, delegation?.toRecord(walletId, account.address, updatedAt))
32+
val stake = getStake.await()
33+
val tokenResults = getTokens.await()
3834

39-
val tokens = getTokens.await()
35+
val nativeResult = updateNativeBalance(walletId, account, updatedAt, native, stake)
4036

41-
listOfNotNull(fullNative) + tokens
37+
listOfNotNull(nativeResult) + tokenResults
4238
}
4339

44-
private suspend fun updateNativeBalance(walletId: String, account: Account, updatedAt: Long): DbBalance? {
45-
val prevBalance =
46-
balancesDao.getByAccount(walletId, account.address, account.chain.string)
47-
val nativeBalance = balancesService.getNativeBalances(account)
48-
val dbNativeBalance = DbBalance.mergeNative(
49-
prevBalance,
50-
nativeBalance?.toRecord(walletId, account.address, updatedAt),
51-
)
52-
dbNativeBalance?.let { runCatching { balancesDao.insert(it) } }
53-
return dbNativeBalance
40+
private fun updateNativeBalance(
41+
walletId: String,
42+
account: Account,
43+
updatedAt: Long,
44+
native: AssetBalance?,
45+
stake: AssetBalance?,
46+
): AssetBalance? {
47+
val assetId = account.chain.string
48+
49+
if (native == null && stake == null) {
50+
return balancesDao.getByAccount(walletId, account.address, assetId)?.toDTO()
51+
}
52+
53+
ensureRowExists(walletId, account.address, assetId, updatedAt)
54+
55+
native?.let {
56+
balancesDao.updateCoinBalance(
57+
walletId = walletId,
58+
address = account.address,
59+
assetId = assetId,
60+
available = it.balance.available,
61+
availableAmount = it.balanceAmount.available,
62+
reserved = it.balance.reserved,
63+
reservedAmount = it.balanceAmount.reserved,
64+
isActive = it.isActive,
65+
updatedAt = updatedAt,
66+
)
67+
}
68+
69+
stake?.let {
70+
balancesDao.updateStakeBalance(
71+
walletId = walletId,
72+
address = account.address,
73+
assetId = assetId,
74+
staked = it.balance.staked,
75+
stakedAmount = it.balanceAmount.staked,
76+
frozen = it.balance.frozen,
77+
frozenAmount = it.balanceAmount.frozen,
78+
locked = it.balance.locked,
79+
lockedAmount = it.balanceAmount.locked,
80+
pending = it.balance.pending,
81+
pendingAmount = it.balanceAmount.pending,
82+
rewards = it.balance.rewards,
83+
rewardsAmount = it.balanceAmount.rewards,
84+
votes = it.metadata?.votes?.toLong() ?: 0L,
85+
energyAvailable = it.metadata?.energyAvailable?.toLong() ?: 0L,
86+
energyTotal = it.metadata?.energyTotal?.toLong() ?: 0L,
87+
bandwidthAvailable = it.metadata?.bandwidthAvailable?.toLong() ?: 0L,
88+
bandwidthTotal = it.metadata?.bandwidthTotal?.toLong() ?: 0L,
89+
updatedAt = updatedAt,
90+
)
91+
}
92+
93+
return balancesDao.getByAccount(walletId, account.address, assetId)?.toDTO()
5494
}
5595

56-
private suspend fun updateTokensBalance(walletId: String, account: Account, tokens: List<Asset>, updatedAt: Long): List<AssetBalance> {
96+
private suspend fun updateTokensBalance(
97+
walletId: String,
98+
account: Account,
99+
tokens: List<Asset>,
100+
updatedAt: Long,
101+
): List<AssetBalance> {
57102
if (tokens.none { it.id.tokenId != null }) return emptyList()
58103
val balances = balancesService.getTokensBalances(account, tokens)
59-
runCatching {
60-
val record = balances.map {
61-
it.toRecord(walletId, account.address, updatedAt)
62-
}
63-
balancesDao.insert(record)
104+
for (balance in balances) {
105+
val assetId = balance.asset.id.toIdentifier()
106+
ensureRowExists(walletId, account.address, assetId, updatedAt)
107+
balancesDao.updateTokenBalance(
108+
walletId = walletId,
109+
address = account.address,
110+
assetId = assetId,
111+
available = balance.balance.available,
112+
availableAmount = balance.balanceAmount.available,
113+
isActive = balance.isActive,
114+
updatedAt = updatedAt,
115+
)
64116
}
65117
return balances
66118
}
67119

68-
private suspend fun mergeNativeBalances(native: DbBalance?, delegation: DbBalance?): AssetBalance? = withContext(Dispatchers.IO) {
69-
val dbFullBalance = DbBalance.mergeDelegation(native, delegation)
70-
dbFullBalance?.let { runCatching { balancesDao.insert(it) } }
71-
dbFullBalance?.toDTO()
120+
private fun ensureRowExists(walletId: String, address: String, assetId: String, updatedAt: Long) {
121+
balancesDao.insertIgnore(
122+
DbBalance(assetId = assetId, walletId = walletId, accountAddress = address, updatedAt = updatedAt)
123+
)
72124
}
73-
}
125+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.gemwallet.android.data.repositories.assets
2+
3+
import com.gemwallet.android.blockchain.services.BalancesService
4+
import com.gemwallet.android.data.service.store.database.BalancesDao
5+
import com.gemwallet.android.data.service.store.database.entities.DbBalance
6+
import com.gemwallet.android.ext.asset
7+
import com.gemwallet.android.testkit.mockAccount
8+
import com.gemwallet.android.testkit.mockAssetEthereum
9+
import com.wallet.core.primitives.Chain
10+
import io.mockk.coEvery
11+
import io.mockk.every
12+
import io.mockk.mockk
13+
import io.mockk.mockkStatic
14+
import io.mockk.unmockkAll
15+
import io.mockk.verify
16+
import kotlinx.coroutines.test.runTest
17+
import org.junit.After
18+
import org.junit.Assert.assertEquals
19+
import org.junit.Assert.assertTrue
20+
import org.junit.Test
21+
22+
class UpdateBalancesTest {
23+
24+
private val balancesDao = mockk<BalancesDao>(relaxed = true)
25+
private val balancesService = mockk<BalancesService>(relaxed = true)
26+
27+
private val subject = UpdateBalances(
28+
balancesDao = balancesDao,
29+
balancesService = balancesService,
30+
)
31+
32+
@After
33+
fun tearDown() {
34+
unmockkAll()
35+
}
36+
37+
@Test
38+
fun `failed native refresh does not create empty balance row`() = runTest {
39+
val walletId = "wallet-1"
40+
val account = mockAccount(chain = Chain.Ethereum)
41+
42+
every { balancesDao.getByAccount(walletId, account.address, account.chain.string) } returns null
43+
coEvery { balancesService.getNativeBalances(account) } returns null
44+
coEvery { balancesService.getStakeBalances(account) } returns null
45+
46+
val result = subject.updateBalances(walletId, account, emptyList())
47+
48+
assertTrue(result.isEmpty())
49+
verify(exactly = 0) { balancesDao.insertIgnore(any()) }
50+
verify(exactly = 0) {
51+
balancesDao.updateCoinBalance(any(), any(), any(), any(), any(), any(), any(), any(), any())
52+
}
53+
}
54+
55+
@Test
56+
fun `failed native refresh keeps previous balance row`() = runTest {
57+
val walletId = "wallet-1"
58+
val account = mockAccount(chain = Chain.Ethereum)
59+
val existing = DbBalance(
60+
assetId = account.chain.string,
61+
walletId = walletId,
62+
accountAddress = account.address,
63+
available = "1000000000000000000",
64+
availableAmount = 1.0,
65+
updatedAt = 1L,
66+
)
67+
68+
mockkStatic("com.gemwallet.android.ext.ChainKt")
69+
every { Chain.Ethereum.asset() } returns mockAssetEthereum()
70+
every { balancesDao.getByAccount(walletId, account.address, account.chain.string) } returns existing
71+
coEvery { balancesService.getNativeBalances(account) } returns null
72+
coEvery { balancesService.getStakeBalances(account) } returns null
73+
74+
val result = subject.updateBalances(walletId, account, emptyList())
75+
76+
assertEquals(1, result.size)
77+
assertEquals("1000000000000000000", result.single().balance.available)
78+
verify(exactly = 0) { balancesDao.insertIgnore(any()) }
79+
verify(exactly = 0) {
80+
balancesDao.updateCoinBalance(any(), any(), any(), any(), any(), any(), any(), any(), any())
81+
}
82+
}
83+
}

android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/BalancesDao.kt

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,97 @@ interface BalancesDao {
1515
@Insert(onConflict = OnConflictStrategy.REPLACE)
1616
fun insert(balance: List<DbBalance>)
1717

18+
@Insert(onConflict = OnConflictStrategy.IGNORE)
19+
fun insertIgnore(balance: DbBalance)
20+
1821
@Update
1922
fun update(balance: DbBalance)
2023

2124
@Query("SELECT * FROM balances WHERE wallet_id = :walletId AND account_address = :accountAddresses AND asset_id = :assetId")
2225
fun getByAccount(walletId: String, accountAddresses: String, assetId: String): DbBalance?
26+
27+
@Query("""
28+
UPDATE balances SET
29+
available = :available,
30+
available_amount = :availableAmount,
31+
reserved = :reserved,
32+
reserved_amount = :reservedAmount,
33+
total_amount = :availableAmount + frozen_amount + locked_amount + staked_amount + pending_amount + rewards_amount,
34+
updated_at = :updatedAt,
35+
is_active = :isActive
36+
WHERE wallet_id = :walletId AND account_address = :address AND asset_id = :assetId
37+
""")
38+
fun updateCoinBalance(
39+
walletId: String,
40+
address: String,
41+
assetId: String,
42+
available: String,
43+
availableAmount: Double,
44+
reserved: String,
45+
reservedAmount: Double,
46+
isActive: Boolean,
47+
updatedAt: Long,
48+
)
49+
50+
@Query("""
51+
UPDATE balances SET
52+
available = :available,
53+
available_amount = :availableAmount,
54+
total_amount = :availableAmount + frozen_amount + locked_amount + staked_amount + pending_amount + rewards_amount,
55+
updated_at = :updatedAt,
56+
is_active = :isActive
57+
WHERE wallet_id = :walletId AND account_address = :address AND asset_id = :assetId
58+
""")
59+
fun updateTokenBalance(
60+
walletId: String,
61+
address: String,
62+
assetId: String,
63+
available: String,
64+
availableAmount: Double,
65+
isActive: Boolean,
66+
updatedAt: Long,
67+
)
68+
69+
@Query("""
70+
UPDATE balances SET
71+
staked = :staked,
72+
staked_amount = :stakedAmount,
73+
frozen = :frozen,
74+
frozen_amount = :frozenAmount,
75+
locked = :locked,
76+
locked_amount = :lockedAmount,
77+
pending = :pending,
78+
pending_amount = :pendingAmount,
79+
rewards = :rewards,
80+
rewards_amount = :rewardsAmount,
81+
votes = :votes,
82+
energy_available = :energyAvailable,
83+
energy_total = :energyTotal,
84+
bandwidth_available = :bandwidthAvailable,
85+
bandwidth_total = :bandwidthTotal,
86+
total_amount = available_amount + :frozenAmount + :lockedAmount + :stakedAmount + :pendingAmount + :rewardsAmount,
87+
updated_at = :updatedAt
88+
WHERE wallet_id = :walletId AND account_address = :address AND asset_id = :assetId
89+
""")
90+
fun updateStakeBalance(
91+
walletId: String,
92+
address: String,
93+
assetId: String,
94+
staked: String,
95+
stakedAmount: Double,
96+
frozen: String,
97+
frozenAmount: Double,
98+
locked: String,
99+
lockedAmount: Double,
100+
pending: String,
101+
pendingAmount: Double,
102+
rewards: String,
103+
rewardsAmount: Double,
104+
votes: Long,
105+
energyAvailable: Long,
106+
energyTotal: Long,
107+
bandwidthAvailable: Long,
108+
bandwidthTotal: Long,
109+
updatedAt: Long,
110+
)
23111
}

0 commit comments

Comments
 (0)