Skip to content

Commit 2023870

Browse files
committed
fix(tokens): batch metadata fetching to reduce getMintMetadata RPC calls
Replace per-mint getMintMetadata calls during update() with a single batched RPC call via refreshAllMetadata(). Remove refreshMetadataInBackground which was firing individual network calls on every cache hit. - Add getTokenMetadata(List<Mint>) to TokenController; single-mint overload now delegates to it - TokenCoordinator.updateTokens() batch-refreshes all known mints before building token balances, so cache hits avoid network calls - Remove refreshMetadataInBackground and refreshingMints tracking - TokenController no longer implements TokenMetadataProvider directly Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent d5d394c commit 2023870

3 files changed

Lines changed: 55 additions & 54 deletions

File tree

apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.flipcash.app.core.extensions.setText
77
import com.flipcash.features.deposit.R
88
import com.flipcash.services.user.UserManager
99
import com.getcode.manager.BottomBarManager
10-
import com.getcode.opencode.controllers.TokenController
10+
import com.getcode.opencode.providers.TokenMetadataProvider
1111
import com.getcode.solana.keys.Mint
1212
import com.getcode.solana.keys.base58
1313
import com.getcode.util.resources.ResourceHelper
@@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds
2424
@HiltViewModel
2525
internal class DepositViewModel @Inject constructor(
2626
userManager: UserManager,
27-
tokenController: TokenController,
27+
tokenController: TokenMetadataProvider,
2828
clipboardManager: ClipboardManager,
2929
resources: ResourceHelper,
3030
) : BaseViewModel2<DepositViewModel.State, DepositViewModel.Event>(

apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenCoordinator.kt

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ class TokenCoordinator @Inject constructor(
100100

101101
private val cluster = MutableStateFlow<AccountCluster?>(null)
102102
private val fetchingMints = ConcurrentHashMap.newKeySet<Mint>()
103-
private val refreshingMints = ConcurrentHashMap.newKeySet<Mint>()
104103

105104
data class TokenState(
106105
val tokens: Map<Mint, Token> = emptyMap(),
@@ -225,7 +224,6 @@ class TokenCoordinator @Inject constructor(
225224
// 1. In-memory cache
226225
_state.value.tokens[mint]?.let { cached ->
227226
trace(tag = TAG, message = "Token metadata memory hit for ${cached.symbol}", type = TraceType.Silent)
228-
refreshMetadataInBackground(mint)
229227
return Result.success(TokenResult(cached, DataSource.Memory))
230228
}
231229

@@ -235,7 +233,6 @@ class TokenCoordinator @Inject constructor(
235233
_state.update { state ->
236234
state.copy(tokens = state.tokens + (persisted.address to persisted))
237235
}
238-
refreshMetadataInBackground(mint)
239236
return Result.success(TokenResult(persisted, DataSource.Cache))
240237
}
241238

@@ -257,42 +254,6 @@ class TokenCoordinator @Inject constructor(
257254
}
258255
}
259256

260-
/**
261-
* Fires a background network fetch for fresh metadata.
262-
* If the response differs from the cached version, both in-memory
263-
* state and Room are updated so the UI picks up changes.
264-
*/
265-
private fun refreshMetadataInBackground(mint: Mint) {
266-
if (!refreshingMints.add(mint)) return // already refreshing
267-
268-
scope.launch {
269-
try {
270-
tokenController.getTokenMetadata(mint)
271-
.onSuccess { result ->
272-
val fresh = result.token
273-
val cached = _state.value.tokens[mint]
274-
275-
// Only update if metadata actually changed
276-
if (cached != null && fresh == cached) return@launch
277-
278-
trace(tag = TAG, message = "Background refresh updated metadata for ${fresh.symbol}", type = TraceType.Silent)
279-
280-
_state.update { state ->
281-
state.copy(tokens = state.tokens + (fresh.address to fresh))
282-
}
283-
284-
if (accountController.hasAccountFor(fresh.address)) {
285-
dataSource.upsert(listOf(fresh))
286-
}
287-
}
288-
} finally {
289-
refreshingMints.remove(mint)
290-
}
291-
}
292-
}
293-
294-
295-
296257
// endregion
297258

298259
// region Public API — Selection & Updates
@@ -366,7 +327,12 @@ class TokenCoordinator @Inject constructor(
366327

367328
trace(tag = TAG, message = "Fetching all token accounts", type = TraceType.Process)
368329

369-
// Pass `this` as metadataProvider so network fetches benefit from memory/Room cache
330+
// Batch-refresh metadata for all known mints in a single RPC call,
331+
// hydrating memory and Room so that fetchTokenAccounts hits warm caches
332+
// without triggering per-mint background refreshes.
333+
refreshAllMetadata()
334+
335+
// Pass `this` as metadataProvider so fetches benefit from the now-warm cache
370336
tokenController.fetchTokenAccounts(owner, metadataProvider = this)
371337
.onSuccess { updates ->
372338
trace(tag = TAG, message = "Successfully built ${updates.size} token balances", type = TraceType.Process)
@@ -379,6 +345,31 @@ class TokenCoordinator @Inject constructor(
379345
}
380346
}
381347

348+
/**
349+
* Batch-fetches fresh metadata for all known mints in a single RPC call
350+
* and updates both in-memory state and Room persistence for any changes.
351+
*/
352+
private suspend fun refreshAllMetadata() {
353+
val mints = _state.value.tokens.keys.toList()
354+
if (mints.isEmpty()) return
355+
356+
val freshMetadata = tokenController.getTokenMetadata(mints)
357+
.getOrNull() ?: return
358+
359+
trace(tag = TAG, message = "Batch-refreshed metadata for ${freshMetadata.size} mint(s)", type = TraceType.Process)
360+
361+
val changed = freshMetadata.filter { fresh ->
362+
_state.value.tokens[fresh.address] != fresh
363+
}
364+
365+
if (changed.isNotEmpty()) {
366+
_state.update { state ->
367+
state.copy(tokens = state.tokens + changed.associateBy { it.address })
368+
}
369+
dataSource.upsert(changed)
370+
}
371+
}
372+
382373
suspend fun updateTokenAccount(mint: Mint) {
383374
val owner = cluster.value ?: run {
384375
trace(tag = TAG, message = "Cannot update token account: no authenticated user", type = TraceType.Error)

services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TokenController.kt

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import com.getcode.opencode.model.accounts.AccountFilter
77
import com.getcode.opencode.model.accounts.AccountInfo
88
import com.getcode.opencode.model.accounts.AccountType
99
import com.getcode.opencode.model.financial.CurrencyCode
10+
import com.getcode.opencode.model.financial.DataSource
1011
import com.getcode.opencode.model.financial.Fiat
1112
import com.getcode.opencode.model.financial.HistoricalMintData
13+
import com.getcode.opencode.model.financial.MintMetadata
1214
import com.getcode.opencode.model.financial.TokenResult
1315
import com.getcode.opencode.model.financial.TokenWithBalance
1416
import com.getcode.opencode.model.financial.minus
@@ -39,14 +41,12 @@ import javax.inject.Singleton
3941
class TokenController @Inject constructor(
4042
private val accountController: AccountController,
4143
private val currencyController: CurrencyController,
42-
) : TokenMetadataProvider {
44+
) {
4345

4446
companion object {
4547
private const val TAG = "TokenController"
4648
}
4749

48-
// region TokenMetadataProvider
49-
5050
/**
5151
* Fetches token metadata directly from the network.
5252
*
@@ -56,30 +56,40 @@ class TokenController @Inject constructor(
5656
* @param mint The [Mint] address of the token to fetch metadata for.
5757
* @return A [Result] containing a [TokenResult] on success, or an error on failure.
5858
*/
59-
override suspend fun getTokenMetadata(mint: Mint): Result<TokenResult> {
59+
suspend fun getTokenMetadata(mint: Mint): Result<TokenResult> {
60+
return getTokenMetadata(listOf(mint))
61+
.mapCatching { it.first { t -> t.address == mint } }
62+
.map { TokenResult(it, DataSource.Network) }
63+
}
64+
65+
/**
66+
* Batch-fetches token metadata for multiple mints in a single RPC call.
67+
*
68+
* @param mints The list of [Mint] addresses to fetch metadata for.
69+
* @return A [Result] containing the list of [MintMetadata] on success.
70+
*/
71+
suspend fun getTokenMetadata(mints: List<Mint>): Result<List<MintMetadata>> {
6072
trace(
6173
tag = TAG,
62-
message = "Fetching token metadata for ${mint.base58()} from network",
74+
message = "Batch-fetching token metadata for ${mints.size} mint(s) from network",
6375
type = TraceType.Process
6476
)
6577

66-
return currencyController.getMintMetadata(listOf(mint))
78+
return currencyController.getMintMetadata(mints)
6779
.onSuccess { tokens ->
6880
trace(
6981
tag = TAG,
70-
message = "Fetched metadata for ${tokens.size} token(s)",
82+
message = "Batch-fetched metadata for ${tokens.size} token(s)",
7183
type = TraceType.Process
7284
)
7385
}
7486
.onFailure { error ->
7587
trace(
7688
tag = TAG,
77-
message = "Failed to fetch token metadata for ${mint.base58()}: ${error.message}",
89+
message = "Failed to batch-fetch token metadata: ${error.message}",
7890
type = TraceType.Error
7991
)
8092
}
81-
.mapCatching { it.first { t -> t.address == mint } }
82-
.map { TokenResult(it, com.getcode.opencode.model.financial.DataSource.Network) }
8393
}
8494

8595
// endregion
@@ -98,7 +108,7 @@ class TokenController @Inject constructor(
98108
*/
99109
suspend fun fetchTokenAccounts(
100110
cluster: AccountCluster,
101-
metadataProvider: TokenMetadataProvider = this,
111+
metadataProvider: TokenMetadataProvider,
102112
): Result<List<TokenWithBalance>> {
103113
trace(tag = TAG, message = "Fetching all primary token accounts", type = TraceType.Process)
104114

@@ -132,7 +142,7 @@ class TokenController @Inject constructor(
132142
suspend fun fetchTokenAccount(
133143
cluster: AccountCluster,
134144
mint: Mint,
135-
metadataProvider: TokenMetadataProvider = this,
145+
metadataProvider: TokenMetadataProvider,
136146
): Result<TokenWithBalance> {
137147
trace(tag = TAG, message = "Fetching token account for ${mint.base58()}", type = TraceType.Process)
138148

0 commit comments

Comments
 (0)