Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pulse Asset Value Calculation Tweaks, Mild Code Reformatting #596

Merged
merged 2 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,35 @@ Ref: https://keepachangelog.com/en/1.0.0/

## Unreleased

## [v7.0.2](https://github.com/provenance-io/explorer-service/releases/tag/v7.0.2) - 2025-02-27

Version 7.0.2 of Explorer Service fixes asset value bugs related to how metadata
NAV USD decimals are handled and committed valuations are calculated.

## Bug Fixes

* Use 3 decimals for `metadata` NAV events when calculating USD value
* Get the exchange module-based price when valuing committed assets and
ensure denom metadata is used to calculate the denom's volume.

## [v7.0.1](https://github.com/provenance-io/explorer-service/releases/tag/v7.0.1) - 2025-02-26

## Upgrades

Version 7.0.1 of Explorer introduces new endpoints for the Provenance Pulse.
Provenance Pulse is a new web application that rolls up the high-level value,
volume, exchange, and transactions associated with assets on the chain. It is
not intended to replace the Provenance Explorer. Instead, it's purpose is to
provide fast visualization of the value of Provenance assets.

**Highlights**

"Today compared to Yesterday" metrics for fast, simple visualizations
Daily metrics are maintained in DB for future, longer timespan metrics
Metrics include value, volume, etc trends
Metrics can include series data for chart visualizations
Accompanied by a Provenance Pulse web application.

## [v7.0.0](https://github.com/provenance-io/explorer-service/releases/tag/v7.0.0) - 2025-02-12

## Upgrades
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.provenance.explorer.config.ExplorerProperties.Companion.UTILITY_TOKEN_
import io.provenance.explorer.config.ResourceNotFoundException
import io.provenance.explorer.domain.core.logger
import io.provenance.explorer.domain.entities.AccountRecord
import io.provenance.explorer.domain.entities.NavEvent
import io.provenance.explorer.domain.entities.NavEventsRecord
import io.provenance.explorer.domain.entities.PulseCacheRecord
import io.provenance.explorer.domain.entities.TxCacheRecord
Expand Down Expand Up @@ -170,30 +171,21 @@ class PulseMetricService(
private fun isScheduledTask(): Boolean =
Thread.currentThread().name.startsWith("scheduling-")

/**
* Periodically refreshes the pulse cache
*/
fun refreshCache() = transaction {
val threadName = Thread.currentThread().name
logger.info("Refreshing pulse cache for thread $threadName")
PulseCacheType.entries.filter {
it != PulseCacheType.PULSE_ASSET_VOLUME_SUMMARY_METRIC &&
it != PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC
}
.forEach { type ->
pulseMetric(type)
}

pulseAssetSummaries()

logger.info("Pulse cache refreshed for thread $threadName")
}

private fun denomSupplyCache(denom: String) =
denomCurrentSupplyCache.get(denom) {
assetService.getCurrentSupply(denom).toBigDecimal()
} ?: BigDecimal.ZERO

private fun latestPulseAssetPrice(denom: String) =
fromPulseMetricCache(
LocalDateTime.now().startOfDay().toLocalDate(),
PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC, denom
)?.amount ?:
fromPulseMetricCache(
LocalDateTime.now().minusDays(1).startOfDay().toLocalDate(),
PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC, denom
)?.amount ?: BigDecimal.ZERO

/**
* Returns the current hash market cap metric comparing the previous day's market cap
* to the current day's market cap
Expand All @@ -214,56 +206,6 @@ class PulseMetricService(
?: throw ResourceNotFoundException("No quote found for $quote")
}

/**
* Returns the current hash metrics for the given type
*/
fun hashMetric(type: PulseCacheType, bustCache: Boolean = false) =
fetchOrBuildCacheFromDataSource(
type = type
) {
val latestHashPrice =
tokenService.getTokenLatest()?.quote?.get(USD_UPPER)?.price
?: BigDecimal.ZERO

when (type) {
PulseCacheType.HASH_STAKED_METRIC -> {
val staked = validatorService.getStakingValidators(ACTIVE)
.sumOf { it.tokenCount }
.divide(UTILITY_TOKEN_BASE_MULTIPLIER).roundWhole()
PulseMetric.build(
base = UTILITY_TOKEN,
amount = staked,
quote = USD_UPPER,
quoteAmount = latestHashPrice.times(staked)
)
}

PulseCacheType.HASH_CIRCULATING_METRIC -> {
val tokenSupply = tokenService.totalSupply()
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
.roundWhole()
PulseMetric.build(
base = UTILITY_TOKEN,
amount = tokenSupply,
quote = USD_UPPER,
quoteAmount = latestHashPrice.times(tokenSupply)
)
}

PulseCacheType.HASH_SUPPLY_METRIC -> {
val tokenSupply = tokenService.maxSupply()
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
.roundWhole()
PulseMetric.build(
base = UTILITY_TOKEN,
amount = tokenSupply
)
}

else -> throw ResourceNotFoundException("Invalid hash metric request for type $type")
}
}

/**
* Returns global market cap aka total AUM
*/
Expand Down Expand Up @@ -327,12 +269,21 @@ class PulseMetricService(
type = PulseCacheType.PULSE_RECEIVABLES_METRIC
) { // TODO technically correct assuming only metadata nav events are receivables
NavEventsRecord.getNavEvents(
fromDate = LocalDateTime.now().startOfDay()
).filter { it.source == "metadata" && it.scopeId != null } // gross
fromDate = LocalDateTime.now().startOfDay(),
source = "metadata",
priceDenoms = listOf(USD_LOWER)
).sortedWith(compareBy<NavEvent> { it.scopeId }.thenByDescending { it.blockTime })
.distinctBy {
// will keep the first occurrence which is the latest price event
it.scopeId
}
.sumOf { it.priceAmount!! }.toBigDecimal().let {
/* so it turns out that the `usd` in metadata nav events
use 3 decimal places - :|
*/
PulseMetric.build(
base = USD_UPPER,
amount = it
amount = it.times(inversePowerOfTen(3))
)
}
}
Expand Down Expand Up @@ -398,51 +349,31 @@ class PulseMetricService(
}

/**
* Returns the total committed assets value across all exchanges - a sum of all commitments
* Returns the total committed assets value across all exchanges
* as a sum of all commitments
*/
private fun exchangeCommittedAssetsValue(): PulseMetric =
fetchOrBuildCacheFromDataSource(
type = PulseCacheType.PULSE_COMMITTED_ASSETS_VALUE_METRIC
) {
committedAssetTotals().values.sumOf { it }.let {
committedAssetTotals().map {
// convert amount to appropriate denom decimal
var dE = denomExponent(it.key)
if (dE == 0 && it.key.lowercase().contains(USD_LOWER)) {
dE = 6
}
Pair(it.key, it.value.times(inversePowerOfTen(dE)))
}.map {
// get price of the asset
Pair(it.first, it.second.times(latestPulseAssetPrice(it.first)))
}.sumOf { it.second }.let {
PulseMetric.build(
base = USD_UPPER,
amount = it.times(inversePowerOfTen(6))
amount = it
)
}
}

private fun todoPulse(): PulseMetric =
PulseMetric.build(
base = UTILITY_TOKEN,
amount = BigDecimal.ZERO
)

/**
* Returns the pulse metric for the given type - pulse metrics are "global"
* metrics that are not specific to Hash
*/
fun pulseMetric(type: PulseCacheType): PulseMetric {
return when (type) {
PulseCacheType.HASH_MARKET_CAP_METRIC -> hashMarketCapMetric()
PulseCacheType.HASH_STAKED_METRIC -> hashMetric(type)
PulseCacheType.HASH_CIRCULATING_METRIC -> hashMetric(type)
PulseCacheType.HASH_SUPPLY_METRIC -> hashMetric(type)
PulseCacheType.PULSE_MARKET_CAP_METRIC -> pulseMarketCap()
PulseCacheType.PULSE_TRANSACTION_VOLUME_METRIC -> transactionVolume()
PulseCacheType.PULSE_RECEIVABLES_METRIC -> pulseReceivableValue()
PulseCacheType.PULSE_TRADE_SETTLEMENT_METRIC -> pulseTradesSettled()
PulseCacheType.PULSE_TRADE_VALUE_SETTLED_METRIC -> pulseTradeValueSettled()
PulseCacheType.PULSE_PARTICIPANTS_METRIC -> totalParticipants()
PulseCacheType.PULSE_COMMITTED_ASSETS_METRIC -> exchangeCommittedAssets()
PulseCacheType.PULSE_COMMITTED_ASSETS_VALUE_METRIC -> exchangeCommittedAssetsValue()
PulseCacheType.PULSE_DEMOCRATIZED_PRIME_POOLS_METRIC -> todoPulse()
PulseCacheType.PULSE_MARGIN_LOANS_METRIC -> todoPulse()
PulseCacheType.PULSE_FEES_AUCTIONS_METRIC -> todoPulse()
else -> throw ResourceNotFoundException("Invalid pulse metric request for type $type")
}
}

/**
* Asset denom metadata from chain
*/
Expand All @@ -457,6 +388,9 @@ class PulseMetricService(
private fun denomExponent(denomMetadata: Bank.Metadata) =
denomMetadata.denomUnitsList.firstOrNull { it.exponent != 0 }?.exponent

private fun denomExponent(denom: String) =
denomExponent(pulseAssetDenomMetadata(denom)) ?: 0

/**
* Returns the inverse power of ten for the given exponent because I
* mostly don't like to divide to move decimal places
Expand Down Expand Up @@ -489,8 +423,108 @@ class PulseMetricService(
exchangeGrpcClient.totalCommittedAssetTotals()
}

private fun todoPulse(): PulseMetric =
PulseMetric.build(
base = UTILITY_TOKEN,
amount = BigDecimal.ZERO
)

/**
* Periodically refreshes the pulse cache
*/
fun refreshCache() = transaction {
val threadName = Thread.currentThread().name
logger.info("Refreshing pulse cache for thread $threadName")
PulseCacheType.entries.filter {
it != PulseCacheType.PULSE_ASSET_VOLUME_SUMMARY_METRIC &&
it != PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC
}
.forEach { type ->
pulseMetric(type)
}

pulseAssetSummaries()

logger.info("Pulse cache refreshed for thread $threadName")
}

/**
* Returns the current hash metrics for the given type
*/
fun hashMetric(type: PulseCacheType, bustCache: Boolean = false) =
fetchOrBuildCacheFromDataSource(
type = type
) {
val latestHashPrice =
tokenService.getTokenLatest()?.quote?.get(USD_UPPER)?.price
?: BigDecimal.ZERO

when (type) {
PulseCacheType.HASH_STAKED_METRIC -> {
val staked = validatorService.getStakingValidators(ACTIVE)
.sumOf { it.tokenCount }
.divide(UTILITY_TOKEN_BASE_MULTIPLIER).roundWhole()
PulseMetric.build(
base = UTILITY_TOKEN,
amount = staked,
quote = USD_UPPER,
quoteAmount = latestHashPrice.times(staked)
)
}

PulseCacheType.HASH_CIRCULATING_METRIC -> {
val tokenSupply = tokenService.totalSupply()
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
.roundWhole()
PulseMetric.build(
base = UTILITY_TOKEN,
amount = tokenSupply,
quote = USD_UPPER,
quoteAmount = latestHashPrice.times(tokenSupply)
)
}

PulseCacheType.HASH_SUPPLY_METRIC -> {
val tokenSupply = tokenService.maxSupply()
.divide(UTILITY_TOKEN_BASE_MULTIPLIER)
.roundWhole()
PulseMetric.build(
base = UTILITY_TOKEN,
amount = tokenSupply
)
}

else -> throw ResourceNotFoundException("Invalid hash metric request for type $type")
}
}

/**
* Returns the pulse metric for the given type - pulse metrics are "global"
* metrics that are not specific to Hash
*/
fun pulseMetric(type: PulseCacheType): PulseMetric {
return when (type) {
PulseCacheType.HASH_MARKET_CAP_METRIC -> hashMarketCapMetric()
PulseCacheType.HASH_STAKED_METRIC -> hashMetric(type)
PulseCacheType.HASH_CIRCULATING_METRIC -> hashMetric(type)
PulseCacheType.HASH_SUPPLY_METRIC -> hashMetric(type)
PulseCacheType.PULSE_MARKET_CAP_METRIC -> pulseMarketCap()
PulseCacheType.PULSE_TRANSACTION_VOLUME_METRIC -> transactionVolume()
PulseCacheType.PULSE_RECEIVABLES_METRIC -> pulseReceivableValue()
PulseCacheType.PULSE_TRADE_SETTLEMENT_METRIC -> pulseTradesSettled()
PulseCacheType.PULSE_TRADE_VALUE_SETTLED_METRIC -> pulseTradeValueSettled()
PulseCacheType.PULSE_PARTICIPANTS_METRIC -> totalParticipants()
PulseCacheType.PULSE_COMMITTED_ASSETS_METRIC -> exchangeCommittedAssets()
PulseCacheType.PULSE_COMMITTED_ASSETS_VALUE_METRIC -> exchangeCommittedAssetsValue()
PulseCacheType.PULSE_DEMOCRATIZED_PRIME_POOLS_METRIC -> todoPulse()
PulseCacheType.PULSE_MARGIN_LOANS_METRIC -> todoPulse()
PulseCacheType.PULSE_FEES_AUCTIONS_METRIC -> todoPulse()
else -> throw ResourceNotFoundException("Invalid pulse metric request for type $type")
}
}

/**
* TODO - this is problematic because it assumes all assets are USD-based
* TODO - this is problematic because it assumes all assets are USD quoted
*/
fun pulseAssetSummaries(): List<PulseAssetSummary> =
committedAssetTotals().keys.distinct().map { denom ->
Expand Down Expand Up @@ -630,11 +664,7 @@ class PulseMetricService(
val denomMetadata = pulseAssetDenomMetadata(denom)
val denomExp = denomExponent(denomMetadata) ?: 1
val denomPow = inversePowerOfTen(denomExp)
val denomPrice =
fromPulseMetricCache(
LocalDateTime.now().minusDays(1).startOfDay().toLocalDate(),
PulseCacheType.PULSE_ASSET_PRICE_SUMMARY_METRIC, denom
)?.amount ?: BigDecimal.ZERO
val denomPrice = latestPulseAssetPrice(denom)

PagedResults(
pages = pr.pages,
Expand Down
Loading