Skip to content

Commit 0eefa8d

Browse files
authored
Pulse Asset Value Calculation Tweaks, Mild Code Reformatting (#596)
* updated committed value calcs to account for crypto denoms and their value - rearranged service function order * updated committed value calcs to account for crypto denoms and their value - rearranged service function order
1 parent 4f8a18e commit 0eefa8d

File tree

2 files changed

+171
-112
lines changed

2 files changed

+171
-112
lines changed

CHANGELOG.md

+29
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,35 @@ Ref: https://keepachangelog.com/en/1.0.0/
3333

3434
## Unreleased
3535

36+
## [v7.0.2](https://github.com/provenance-io/explorer-service/releases/tag/v7.0.2) - 2025-02-27
37+
38+
Version 7.0.2 of Explorer Service fixes asset value bugs related to how metadata
39+
NAV USD decimals are handled and committed valuations are calculated.
40+
41+
## Bug Fixes
42+
43+
* Use 3 decimals for `metadata` NAV events when calculating USD value
44+
* Get the exchange module-based price when valuing committed assets and
45+
ensure denom metadata is used to calculate the denom's volume.
46+
47+
## [v7.0.1](https://github.com/provenance-io/explorer-service/releases/tag/v7.0.1) - 2025-02-26
48+
49+
## Upgrades
50+
51+
Version 7.0.1 of Explorer introduces new endpoints for the Provenance Pulse.
52+
Provenance Pulse is a new web application that rolls up the high-level value,
53+
volume, exchange, and transactions associated with assets on the chain. It is
54+
not intended to replace the Provenance Explorer. Instead, it's purpose is to
55+
provide fast visualization of the value of Provenance assets.
56+
57+
**Highlights**
58+
59+
"Today compared to Yesterday" metrics for fast, simple visualizations
60+
Daily metrics are maintained in DB for future, longer timespan metrics
61+
Metrics include value, volume, etc trends
62+
Metrics can include series data for chart visualizations
63+
Accompanied by a Provenance Pulse web application.
64+
3665
## [v7.0.0](https://github.com/provenance-io/explorer-service/releases/tag/v7.0.0) - 2025-02-12
3766

3867
## Upgrades

service/src/main/kotlin/io/provenance/explorer/service/PulseMetricService.kt

+142-112
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.provenance.explorer.config.ExplorerProperties.Companion.UTILITY_TOKEN_
88
import io.provenance.explorer.config.ResourceNotFoundException
99
import io.provenance.explorer.domain.core.logger
1010
import io.provenance.explorer.domain.entities.AccountRecord
11+
import io.provenance.explorer.domain.entities.NavEvent
1112
import io.provenance.explorer.domain.entities.NavEventsRecord
1213
import io.provenance.explorer.domain.entities.PulseCacheRecord
1314
import io.provenance.explorer.domain.entities.TxCacheRecord
@@ -170,30 +171,21 @@ class PulseMetricService(
170171
private fun isScheduledTask(): Boolean =
171172
Thread.currentThread().name.startsWith("scheduling-")
172173

173-
/**
174-
* Periodically refreshes the pulse cache
175-
*/
176-
fun refreshCache() = transaction {
177-
val threadName = Thread.currentThread().name
178-
logger.info("Refreshing pulse cache for thread $threadName")
179-
PulseCacheType.entries.filter {
180-
it != PulseCacheType.PULSE_ASSET_VOLUME_SUMMARY_METRIC &&
181-
it != PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC
182-
}
183-
.forEach { type ->
184-
pulseMetric(type)
185-
}
186-
187-
pulseAssetSummaries()
188-
189-
logger.info("Pulse cache refreshed for thread $threadName")
190-
}
191-
192174
private fun denomSupplyCache(denom: String) =
193175
denomCurrentSupplyCache.get(denom) {
194176
assetService.getCurrentSupply(denom).toBigDecimal()
195177
} ?: BigDecimal.ZERO
196178

179+
private fun latestPulseAssetPrice(denom: String) =
180+
fromPulseMetricCache(
181+
LocalDateTime.now().startOfDay().toLocalDate(),
182+
PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC, denom
183+
)?.amount ?:
184+
fromPulseMetricCache(
185+
LocalDateTime.now().minusDays(1).startOfDay().toLocalDate(),
186+
PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC, denom
187+
)?.amount ?: BigDecimal.ZERO
188+
197189
/**
198190
* Returns the current hash market cap metric comparing the previous day's market cap
199191
* to the current day's market cap
@@ -214,56 +206,6 @@ class PulseMetricService(
214206
?: throw ResourceNotFoundException("No quote found for $quote")
215207
}
216208

217-
/**
218-
* Returns the current hash metrics for the given type
219-
*/
220-
fun hashMetric(type: PulseCacheType, bustCache: Boolean = false) =
221-
fetchOrBuildCacheFromDataSource(
222-
type = type
223-
) {
224-
val latestHashPrice =
225-
tokenService.getTokenLatest()?.quote?.get(USD_UPPER)?.price
226-
?: BigDecimal.ZERO
227-
228-
when (type) {
229-
PulseCacheType.HASH_STAKED_METRIC -> {
230-
val staked = validatorService.getStakingValidators(ACTIVE)
231-
.sumOf { it.tokenCount }
232-
.divide(UTILITY_TOKEN_BASE_MULTIPLIER).roundWhole()
233-
PulseMetric.build(
234-
base = UTILITY_TOKEN,
235-
amount = staked,
236-
quote = USD_UPPER,
237-
quoteAmount = latestHashPrice.times(staked)
238-
)
239-
}
240-
241-
PulseCacheType.HASH_CIRCULATING_METRIC -> {
242-
val tokenSupply = tokenService.totalSupply()
243-
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
244-
.roundWhole()
245-
PulseMetric.build(
246-
base = UTILITY_TOKEN,
247-
amount = tokenSupply,
248-
quote = USD_UPPER,
249-
quoteAmount = latestHashPrice.times(tokenSupply)
250-
)
251-
}
252-
253-
PulseCacheType.HASH_SUPPLY_METRIC -> {
254-
val tokenSupply = tokenService.maxSupply()
255-
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
256-
.roundWhole()
257-
PulseMetric.build(
258-
base = UTILITY_TOKEN,
259-
amount = tokenSupply
260-
)
261-
}
262-
263-
else -> throw ResourceNotFoundException("Invalid hash metric request for type $type")
264-
}
265-
}
266-
267209
/**
268210
* Returns global market cap aka total AUM
269211
*/
@@ -327,12 +269,21 @@ class PulseMetricService(
327269
type = PulseCacheType.PULSE_RECEIVABLES_METRIC
328270
) { // TODO technically correct assuming only metadata nav events are receivables
329271
NavEventsRecord.getNavEvents(
330-
fromDate = LocalDateTime.now().startOfDay()
331-
).filter { it.source == "metadata" && it.scopeId != null } // gross
272+
fromDate = LocalDateTime.now().startOfDay(),
273+
source = "metadata",
274+
priceDenoms = listOf(USD_LOWER)
275+
).sortedWith(compareBy<NavEvent> { it.scopeId }.thenByDescending { it.blockTime })
276+
.distinctBy {
277+
// will keep the first occurrence which is the latest price event
278+
it.scopeId
279+
}
332280
.sumOf { it.priceAmount!! }.toBigDecimal().let {
281+
/* so it turns out that the `usd` in metadata nav events
282+
use 3 decimal places - :|
283+
*/
333284
PulseMetric.build(
334285
base = USD_UPPER,
335-
amount = it
286+
amount = it.times(inversePowerOfTen(3))
336287
)
337288
}
338289
}
@@ -398,51 +349,31 @@ class PulseMetricService(
398349
}
399350

400351
/**
401-
* Returns the total committed assets value across all exchanges - a sum of all commitments
352+
* Returns the total committed assets value across all exchanges
353+
* as a sum of all commitments
402354
*/
403355
private fun exchangeCommittedAssetsValue(): PulseMetric =
404356
fetchOrBuildCacheFromDataSource(
405357
type = PulseCacheType.PULSE_COMMITTED_ASSETS_VALUE_METRIC
406358
) {
407-
committedAssetTotals().values.sumOf { it }.let {
359+
committedAssetTotals().map {
360+
// convert amount to appropriate denom decimal
361+
var dE = denomExponent(it.key)
362+
if (dE == 0 && it.key.lowercase().contains(USD_LOWER)) {
363+
dE = 6
364+
}
365+
Pair(it.key, it.value.times(inversePowerOfTen(dE)))
366+
}.map {
367+
// get price of the asset
368+
Pair(it.first, it.second.times(latestPulseAssetPrice(it.first)))
369+
}.sumOf { it.second }.let {
408370
PulseMetric.build(
409371
base = USD_UPPER,
410-
amount = it.times(inversePowerOfTen(6))
372+
amount = it
411373
)
412374
}
413375
}
414376

415-
private fun todoPulse(): PulseMetric =
416-
PulseMetric.build(
417-
base = UTILITY_TOKEN,
418-
amount = BigDecimal.ZERO
419-
)
420-
421-
/**
422-
* Returns the pulse metric for the given type - pulse metrics are "global"
423-
* metrics that are not specific to Hash
424-
*/
425-
fun pulseMetric(type: PulseCacheType): PulseMetric {
426-
return when (type) {
427-
PulseCacheType.HASH_MARKET_CAP_METRIC -> hashMarketCapMetric()
428-
PulseCacheType.HASH_STAKED_METRIC -> hashMetric(type)
429-
PulseCacheType.HASH_CIRCULATING_METRIC -> hashMetric(type)
430-
PulseCacheType.HASH_SUPPLY_METRIC -> hashMetric(type)
431-
PulseCacheType.PULSE_MARKET_CAP_METRIC -> pulseMarketCap()
432-
PulseCacheType.PULSE_TRANSACTION_VOLUME_METRIC -> transactionVolume()
433-
PulseCacheType.PULSE_RECEIVABLES_METRIC -> pulseReceivableValue()
434-
PulseCacheType.PULSE_TRADE_SETTLEMENT_METRIC -> pulseTradesSettled()
435-
PulseCacheType.PULSE_TRADE_VALUE_SETTLED_METRIC -> pulseTradeValueSettled()
436-
PulseCacheType.PULSE_PARTICIPANTS_METRIC -> totalParticipants()
437-
PulseCacheType.PULSE_COMMITTED_ASSETS_METRIC -> exchangeCommittedAssets()
438-
PulseCacheType.PULSE_COMMITTED_ASSETS_VALUE_METRIC -> exchangeCommittedAssetsValue()
439-
PulseCacheType.PULSE_DEMOCRATIZED_PRIME_POOLS_METRIC -> todoPulse()
440-
PulseCacheType.PULSE_MARGIN_LOANS_METRIC -> todoPulse()
441-
PulseCacheType.PULSE_FEES_AUCTIONS_METRIC -> todoPulse()
442-
else -> throw ResourceNotFoundException("Invalid pulse metric request for type $type")
443-
}
444-
}
445-
446377
/**
447378
* Asset denom metadata from chain
448379
*/
@@ -457,6 +388,9 @@ class PulseMetricService(
457388
private fun denomExponent(denomMetadata: Bank.Metadata) =
458389
denomMetadata.denomUnitsList.firstOrNull { it.exponent != 0 }?.exponent
459390

391+
private fun denomExponent(denom: String) =
392+
denomExponent(pulseAssetDenomMetadata(denom)) ?: 0
393+
460394
/**
461395
* Returns the inverse power of ten for the given exponent because I
462396
* mostly don't like to divide to move decimal places
@@ -489,8 +423,108 @@ class PulseMetricService(
489423
exchangeGrpcClient.totalCommittedAssetTotals()
490424
}
491425

426+
private fun todoPulse(): PulseMetric =
427+
PulseMetric.build(
428+
base = UTILITY_TOKEN,
429+
amount = BigDecimal.ZERO
430+
)
431+
432+
/**
433+
* Periodically refreshes the pulse cache
434+
*/
435+
fun refreshCache() = transaction {
436+
val threadName = Thread.currentThread().name
437+
logger.info("Refreshing pulse cache for thread $threadName")
438+
PulseCacheType.entries.filter {
439+
it != PulseCacheType.PULSE_ASSET_VOLUME_SUMMARY_METRIC &&
440+
it != PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC
441+
}
442+
.forEach { type ->
443+
pulseMetric(type)
444+
}
445+
446+
pulseAssetSummaries()
447+
448+
logger.info("Pulse cache refreshed for thread $threadName")
449+
}
450+
451+
/**
452+
* Returns the current hash metrics for the given type
453+
*/
454+
fun hashMetric(type: PulseCacheType, bustCache: Boolean = false) =
455+
fetchOrBuildCacheFromDataSource(
456+
type = type
457+
) {
458+
val latestHashPrice =
459+
tokenService.getTokenLatest()?.quote?.get(USD_UPPER)?.price
460+
?: BigDecimal.ZERO
461+
462+
when (type) {
463+
PulseCacheType.HASH_STAKED_METRIC -> {
464+
val staked = validatorService.getStakingValidators(ACTIVE)
465+
.sumOf { it.tokenCount }
466+
.divide(UTILITY_TOKEN_BASE_MULTIPLIER).roundWhole()
467+
PulseMetric.build(
468+
base = UTILITY_TOKEN,
469+
amount = staked,
470+
quote = USD_UPPER,
471+
quoteAmount = latestHashPrice.times(staked)
472+
)
473+
}
474+
475+
PulseCacheType.HASH_CIRCULATING_METRIC -> {
476+
val tokenSupply = tokenService.totalSupply()
477+
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
478+
.roundWhole()
479+
PulseMetric.build(
480+
base = UTILITY_TOKEN,
481+
amount = tokenSupply,
482+
quote = USD_UPPER,
483+
quoteAmount = latestHashPrice.times(tokenSupply)
484+
)
485+
}
486+
487+
PulseCacheType.HASH_SUPPLY_METRIC -> {
488+
val tokenSupply = tokenService.maxSupply()
489+
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
490+
.roundWhole()
491+
PulseMetric.build(
492+
base = UTILITY_TOKEN,
493+
amount = tokenSupply
494+
)
495+
}
496+
497+
else -> throw ResourceNotFoundException("Invalid hash metric request for type $type")
498+
}
499+
}
500+
501+
/**
502+
* Returns the pulse metric for the given type - pulse metrics are "global"
503+
* metrics that are not specific to Hash
504+
*/
505+
fun pulseMetric(type: PulseCacheType): PulseMetric {
506+
return when (type) {
507+
PulseCacheType.HASH_MARKET_CAP_METRIC -> hashMarketCapMetric()
508+
PulseCacheType.HASH_STAKED_METRIC -> hashMetric(type)
509+
PulseCacheType.HASH_CIRCULATING_METRIC -> hashMetric(type)
510+
PulseCacheType.HASH_SUPPLY_METRIC -> hashMetric(type)
511+
PulseCacheType.PULSE_MARKET_CAP_METRIC -> pulseMarketCap()
512+
PulseCacheType.PULSE_TRANSACTION_VOLUME_METRIC -> transactionVolume()
513+
PulseCacheType.PULSE_RECEIVABLES_METRIC -> pulseReceivableValue()
514+
PulseCacheType.PULSE_TRADE_SETTLEMENT_METRIC -> pulseTradesSettled()
515+
PulseCacheType.PULSE_TRADE_VALUE_SETTLED_METRIC -> pulseTradeValueSettled()
516+
PulseCacheType.PULSE_PARTICIPANTS_METRIC -> totalParticipants()
517+
PulseCacheType.PULSE_COMMITTED_ASSETS_METRIC -> exchangeCommittedAssets()
518+
PulseCacheType.PULSE_COMMITTED_ASSETS_VALUE_METRIC -> exchangeCommittedAssetsValue()
519+
PulseCacheType.PULSE_DEMOCRATIZED_PRIME_POOLS_METRIC -> todoPulse()
520+
PulseCacheType.PULSE_MARGIN_LOANS_METRIC -> todoPulse()
521+
PulseCacheType.PULSE_FEES_AUCTIONS_METRIC -> todoPulse()
522+
else -> throw ResourceNotFoundException("Invalid pulse metric request for type $type")
523+
}
524+
}
525+
492526
/**
493-
* TODO - this is problematic because it assumes all assets are USD-based
527+
* TODO - this is problematic because it assumes all assets are USD quoted
494528
*/
495529
fun pulseAssetSummaries(): List<PulseAssetSummary> =
496530
committedAssetTotals().keys.distinct().map { denom ->
@@ -630,11 +664,7 @@ class PulseMetricService(
630664
val denomMetadata = pulseAssetDenomMetadata(denom)
631665
val denomExp = denomExponent(denomMetadata) ?: 1
632666
val denomPow = inversePowerOfTen(denomExp)
633-
val denomPrice =
634-
fromPulseMetricCache(
635-
LocalDateTime.now().minusDays(1).startOfDay().toLocalDate(),
636-
PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC, denom
637-
)?.amount ?: BigDecimal.ZERO
667+
val denomPrice = latestPulseAssetPrice(denom)
638668

639669
PagedResults(
640670
pages = pr.pages,

0 commit comments

Comments
 (0)