Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ class PulseProperties(
val hashHoldersExcludedFromCirculatingSupply: Set<String>,
// denoms to include as private equity in TVL calc
val privateEquityTvlDenoms: List<String>,
val hftExchangeApi: String
val hftExchangeApi: String,
// attribute name for Figure passport KYC accounts included in Pulse TVL HASH calc
val passportAttributeName: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ enum class PulseCacheType {
ENTITY_TOTAL_ASSETS_METRIC,

FIGR_HELOC_CIRCULATING_METRIC,

PASSPORT_HASH_BALANCE_METRIC,
PASSPORT_HASH_TVL_METRIC,
}

enum class EntityType(val displayText: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,20 @@ class AccountGrpcClient(channelUri: URI) {
queryDelegatorDelegationsResponse { }
}

suspend fun getDelegationsAtHeight(address: String, offset: Int, limit: Int, height: Int) =
try {
stakingClient
.addBlockHeightToQuery(height)
.delegatorDelegations(
queryDelegatorDelegationsRequest {
this.delegatorAddr = address
this.pagination = getPagination(offset, limit)
}
)
} catch (e: Exception) {
queryDelegatorDelegationsResponse { }
}

suspend fun getUnbondingDelegations(address: String, offset: Int, limit: Int) =
stakingClient.delegatorUnbondingDelegations(
queryDelegatorUnbondingDelegationsRequest {
Expand All @@ -261,6 +275,11 @@ class AccountGrpcClient(channelUri: URI) {
suspend fun getRewards(delAddr: String) =
distClient.delegationTotalRewards(queryDelegationTotalRewardsRequest { this.delegatorAddress = delAddr })

suspend fun getRewardsAtHeight(delAddr: String, height: Int) =
distClient
.addBlockHeightToQuery(height)
.delegationTotalRewards(queryDelegationTotalRewardsRequest { this.delegatorAddress = delAddr })

suspend fun getCommunityPoolAmount(denom: String, height: Int? = null): String =
distClient
.addBlockHeightToQuery(height)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.provenance.explorer.grpc.v1

import io.grpc.ManagedChannelBuilder
import io.provenance.attribute.v1.Attribute
import io.provenance.attribute.v1.queryAttributeAccountsRequest
import io.provenance.attribute.v1.queryAttributesRequest
import io.provenance.attribute.v1.queryParamsRequest
import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor
Expand Down Expand Up @@ -68,6 +69,32 @@ class AttributeGrpcClient(channelUri: URI) {
return attributes
}

suspend fun getAccountsForAttribute(attributeName: String): Set<String> {
var (offset, limit) = 0 to 100

val results = attrClient.attributeAccounts(
queryAttributeAccountsRequest {
this.attributeName = attributeName
this.pagination = getPagination(offset, limit)
}
)

val total = results.pagination?.total ?: results.accountsCount.toLong()
val accounts = results.accountsList.toMutableList()

while (accounts.size < total) {
offset += limit
attrClient.attributeAccounts(
queryAttributeAccountsRequest {
this.attributeName = attributeName
this.pagination = getPagination(offset, limit)
}
).accountsList.also { accounts.addAll(it) }
}

return accounts.toSet()
}

suspend fun getNamesForAddress(address: String, offset: Int, limit: Int) =
nameClient.reverseLookup(
queryReverseLookupRequest {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.provenance.explorer.service

import com.github.benmanes.caffeine.cache.Caffeine
import io.provenance.explorer.config.ExplorerProperties.Companion.UTILITY_TOKEN
import io.provenance.explorer.config.pulse.PulseProperties
import io.provenance.explorer.domain.core.logger
import io.provenance.explorer.domain.entities.BlockCacheRecord
import io.provenance.explorer.grpc.v1.AccountGrpcClient
import io.provenance.explorer.grpc.v1.AttributeGrpcClient
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.time.LocalDateTime
import java.util.concurrent.TimeUnit

@Service
class PassportHashService(
private val attributeGrpcClient: AttributeGrpcClient,
private val accountGrpcClient: AccountGrpcClient,
private val pulseProperties: PulseProperties,
private val semaphore: Semaphore,
) {
protected val logger = logger(PassportHashService::class)

private val passportAccountsCache =
Caffeine.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.maximumSize(10)
.build<String, Set<String>>()

/**
* Returns passport account addresses, cached for 24 hours.
*/
fun getPassportAccounts(): Set<String> =
passportAccountsCache.get(pulseProperties.passportAttributeName) { attributeName ->
runBlocking {
logger.info("Fetching passport accounts for attribute $attributeName")
attributeGrpcClient.getAccountsForAttribute(attributeName)
}
} ?: emptySet()

/**
* Sums nhash holdings (bank + delegated + rewards) across all passport accounts.
*/
fun sumHashHoldings(accounts: Set<String>, atDateTime: LocalDateTime? = null): BigDecimal {
if (accounts.isEmpty()) {
return BigDecimal.ZERO
}

val height = atDateTime?.let { BlockCacheRecord.getLastBlockBeforeTime(it) }

return runBlocking {
accounts.map { address ->
async {
semaphore.withPermit {
sumHashForAccount(address, height)
}
}
}.awaitAll().sumOf { it }
}
Comment on lines +56 to +64
Comment on lines +49 to +64
}

private suspend fun sumHashForAccount(address: String, height: Int?): BigDecimal {
val bankBalance = if (height != null) {
accountGrpcClient.getAccountBalanceForDenomAtHeight(address, UTILITY_TOKEN, height)
.amount.toBigDecimal()
} else {
accountGrpcClient.getAccountBalanceForDenom(address, UTILITY_TOKEN)
.amount.toBigDecimal()
}

val delegatedBalance = delegationTotalNhash(address, height)
val rewardsBalance = rewardsTotalNhash(address, height)

return bankBalance.add(delegatedBalance).add(rewardsBalance)
}

private suspend fun delegationTotalNhash(address: String, height: Int?): BigDecimal {
var offset = 0
val limit = 100

val results = if (height != null) {
accountGrpcClient.getDelegationsAtHeight(address, offset, limit, height)
} else {
accountGrpcClient.getDelegations(address, offset, limit)
}

val total = results.pagination?.total ?: results.delegationResponsesCount.toLong()
val delegations = results.delegationResponsesList.toMutableList()

while (delegations.size < total) {
offset += limit
val page = if (height != null) {
accountGrpcClient.getDelegationsAtHeight(address, offset, limit, height)
} else {
accountGrpcClient.getDelegations(address, offset, limit)
}
delegations.addAll(page.delegationResponsesList)
}

return delegations
.filter { it.balance.denom == UTILITY_TOKEN }
.sumOf { it.balance.amount.toBigDecimal() }
}

private suspend fun rewardsTotalNhash(address: String, height: Int?): BigDecimal {
val rewards = if (height != null) {
accountGrpcClient.getRewardsAtHeight(address, height)
} else {
accountGrpcClient.getRewards(address)
}

return rewards.totalList
.filter { it.denom == UTILITY_TOKEN }
.sumOf { it.amount.toBigDecimal() }
}
}
Loading
Loading