Skip to content

Commit ed04c2a

Browse files
committed
added provenance pulse web app endpoints and db cache
1 parent 03df665 commit ed04c2a

21 files changed

+1218
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE IF NOT EXISTS pulse_cache
2+
(
3+
id SERIAL PRIMARY KEY,
4+
cache_date DATE NOT NULL,
5+
updated_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
type TEXT NOT NULL,
7+
data JSONB NOT NULL
8+
);
9+
10+
CREATE INDEX IF NOT EXISTS idx_pulse_cache_date ON pulse_cache (cache_date);
11+
CREATE INDEX IF NOT EXISTS idx_pulse_cache_type ON pulse_cache (type);
12+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pulse_cache_date_type ON pulse_cache (cache_date, type);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE pulse_cache
2+
ADD COLUMN subtype TEXT;
3+
4+
CREATE INDEX IF NOT EXISTS idx_pulse_cache_subtype ON pulse_cache (subtype);
5+
DROP INDEX IF EXISTS idx_pulse_cache_date_type;
6+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pulse_cache_date_type_subtype ON pulse_cache (cache_date, type, subtype);

service/src/main/kotlin/io/provenance/explorer/config/CacheConfig.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import java.util.concurrent.TimeUnit
77

88
@Configuration
99
class CacheConfig {
10+
1011
@Bean
1112
fun cacheManager() =
1213
CaffeineCacheManager("responses").apply {
13-
setCaffeine(caffieneConfig())
14+
setCaffeine(caffeineConfig())
1415
}
1516

16-
fun caffieneConfig() =
17+
fun caffeineConfig() =
1718
com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
1819
.expireAfterWrite(30, TimeUnit.SECONDS)
1920
.maximumSize(100)

service/src/main/kotlin/io/provenance/explorer/domain/entities/Accounts.kt

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.jetbrains.exposed.dao.id.EntityID
2828
import org.jetbrains.exposed.dao.id.IdTable
2929
import org.jetbrains.exposed.dao.id.IntIdTable
3030
import org.jetbrains.exposed.sql.Op
31+
import org.jetbrains.exposed.sql.and
3132
import org.jetbrains.exposed.sql.insert
3233
import org.jetbrains.exposed.sql.insertAndGetId
3334
import org.jetbrains.exposed.sql.insertIgnore
@@ -69,6 +70,13 @@ class AccountRecord(id: EntityID<Int>) : IntEntity(id) {
6970
AccountRecord.find { AccountTable.type inList types }.toList()
7071
}
7172

73+
fun countActiveAccounts() = transaction {
74+
AccountRecord.find {
75+
(AccountTable.isContract eq Op.FALSE) and
76+
(AccountTable.baseAccount.isNotNull()) and
77+
(AccountTable.type eq "BaseAccount")
78+
}.count()
79+
}
7280
fun findContractAccounts() = transaction {
7381
AccountRecord.find { AccountTable.isContract eq Op.TRUE }.toList()
7482
}

service/src/main/kotlin/io/provenance/explorer/domain/entities/ExplorerCache.kt

+54-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package io.provenance.explorer.domain.entities
22

33
import io.provenance.explorer.OBJECT_MAPPER
44
import io.provenance.explorer.domain.core.sql.jsonb
5+
import io.provenance.explorer.domain.models.explorer.pulse.PulseCacheType
6+
import io.provenance.explorer.domain.models.explorer.pulse.PulseMetric
57
import io.provenance.explorer.model.ChainAum
68
import io.provenance.explorer.model.ChainMarketRate
79
import io.provenance.explorer.model.CmcHistoricalQuote
@@ -14,6 +16,7 @@ import org.jetbrains.exposed.dao.IntEntityClass
1416
import org.jetbrains.exposed.dao.id.EntityID
1517
import org.jetbrains.exposed.dao.id.IdTable
1618
import org.jetbrains.exposed.dao.id.IntIdTable
19+
import org.jetbrains.exposed.sql.Column
1720
import org.jetbrains.exposed.sql.SortOrder
1821
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
1922
import org.jetbrains.exposed.sql.and
@@ -278,7 +281,7 @@ class ProcessQueueRecord(id: EntityID<Int>) : IntEntity(id) {
278281
fun findByType(processType: ProcessQueueType) = transaction {
279282
ProcessQueueRecord.find {
280283
(ProcessQueueTable.processType eq processType.name) and
281-
(ProcessQueueTable.processing eq false)
284+
(ProcessQueueTable.processing eq false)
282285
}.toList()
283286
}
284287

@@ -291,19 +294,63 @@ class ProcessQueueRecord(id: EntityID<Int>) : IntEntity(id) {
291294
fun delete(processType: ProcessQueueType, value: String) = transaction {
292295
ProcessQueueTable.deleteWhere {
293296
(ProcessQueueTable.processType eq processType.name) and
294-
(processValue eq value)
297+
(processValue eq value)
295298
}
296299
}
297300

298-
fun insertIgnore(processType: ProcessQueueType, processValue: String) = transaction {
299-
ProcessQueueTable.insertIgnore {
300-
it[this.processType] = processType.name
301-
it[this.processValue] = processValue
301+
fun insertIgnore(processType: ProcessQueueType, processValue: String) =
302+
transaction {
303+
ProcessQueueTable.insertIgnore {
304+
it[this.processType] = processType.name
305+
it[this.processValue] = processValue
306+
}
302307
}
303-
}
304308
}
305309

306310
var processType by ProcessQueueTable.processType
307311
var processValue by ProcessQueueTable.processValue
308312
var processing by ProcessQueueTable.processing
309313
}
314+
315+
object PulseCacheTable : IntIdTable(name = "pulse_cache") {
316+
val cacheDate = date("cache_date")
317+
val updatedTimestamp = datetime("updated_timestamp")
318+
val data = jsonb<PulseCacheTable, PulseMetric>("data", OBJECT_MAPPER)
319+
val type: Column<PulseCacheType> = enumerationByName("type", 128, PulseCacheType::class)
320+
val subtype = text("subtype").nullable()
321+
}
322+
323+
class PulseCacheRecord(id: EntityID<Int>) : IntEntity(id) {
324+
companion object : IntEntityClass<PulseCacheRecord>(
325+
PulseCacheTable
326+
) {
327+
328+
fun upsert(date: LocalDate, type: PulseCacheType, data: PulseMetric, subtype: String? = null) = transaction {
329+
findByDateAndType(date, type, subtype)?.apply {
330+
this.data = data
331+
this.updatedTimestamp = LocalDateTime.now()
332+
} ?:
333+
PulseCacheTable.insertIgnore {
334+
it[this.cacheDate] = date
335+
it[this.updatedTimestamp] = LocalDateTime.now()
336+
it[this.type] = type
337+
it[this.subtype] = subtype
338+
it[this.data] = data
339+
}
340+
}
341+
342+
fun findByDateAndType(date: LocalDate, type: PulseCacheType, subtype: String? = null) =
343+
transaction {
344+
PulseCacheRecord.find {
345+
(PulseCacheTable.cacheDate eq date) and
346+
(PulseCacheTable.type eq type) and
347+
(if (subtype != null) PulseCacheTable.subtype eq subtype else PulseCacheTable.subtype.isNull())
348+
}.firstOrNull()
349+
}
350+
}
351+
352+
var type by PulseCacheTable.type
353+
var data by PulseCacheTable.data
354+
var updatedTimestamp by PulseCacheTable.updatedTimestamp
355+
var subtype by PulseCacheTable.subtype
356+
}

service/src/main/kotlin/io/provenance/explorer/domain/entities/NavEvents.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ class NavEventsRecord(id: EntityID<Int>) : IntEntity(id) {
6767
scopeId: String? = null,
6868
fromDate: LocalDateTime? = null,
6969
toDate: LocalDateTime? = null,
70-
priceDenoms: List<String>? = null
70+
priceDenoms: List<String>? = null,
71+
source: String? = null
7172
) = transaction {
7273
var query = """
7374
SELECT block_height, block_time, tx_hash, event_order,
@@ -106,6 +107,11 @@ class NavEventsRecord(id: EntityID<Int>) : IntEntity(id) {
106107
}
107108
}
108109

110+
source?.let {
111+
query += " AND source = ?"
112+
args.add(Pair(VarCharColumnType(), it))
113+
}
114+
109115
query += " ORDER BY block_height DESC, event_order DESC"
110116

111117
query.execAndMap(args) {

service/src/main/kotlin/io/provenance/explorer/domain/entities/Transactions.kt

+89-19
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ import io.provenance.explorer.config.ExplorerProperties
99
import io.provenance.explorer.domain.core.logger
1010
import io.provenance.explorer.domain.core.sql.jsonb
1111
import io.provenance.explorer.domain.core.sql.toProcedureObject
12-
import io.provenance.explorer.domain.entities.FeeType.BASE_FEE_OVERAGE
13-
import io.provenance.explorer.domain.entities.FeeType.BASE_FEE_USED
14-
import io.provenance.explorer.domain.entities.FeeType.CUSTOM_FEE
15-
import io.provenance.explorer.domain.entities.FeeType.MSG_BASED_FEE
1612
import io.provenance.explorer.domain.extensions.CUSTOM_FEE_MSG_TYPE
1713
import io.provenance.explorer.domain.extensions.exec
1814
import io.provenance.explorer.domain.extensions.execAndMap
@@ -43,6 +39,7 @@ import io.provenance.explorer.model.TxAssociatedValues
4339
import io.provenance.explorer.model.TxFeepayer
4440
import io.provenance.explorer.model.TxGasVolume
4541
import io.provenance.explorer.model.TxStatus
42+
import io.provenance.explorer.model.base.PagedResults
4643
import io.provenance.explorer.model.base.stringfy
4744
import io.provenance.explorer.service.AssetService
4845
import org.jetbrains.exposed.dao.IntEntity
@@ -69,6 +66,7 @@ import org.jetbrains.exposed.sql.selectAll
6966
import org.jetbrains.exposed.sql.transactions.TransactionManager
7067
import org.jetbrains.exposed.sql.transactions.transaction
7168
import java.math.BigDecimal
69+
import java.time.LocalDate
7270
import java.time.LocalDateTime
7371
import java.time.format.DateTimeFormatter
7472

@@ -151,19 +149,19 @@ class TxCacheRecord(id: EntityID<Int>) : IntEntity(id) {
151149
var join: ColumnSet = TxCacheTable
152150

153151
if (tqp.msgTypes.isNotEmpty()) {
154-
join = join.innerJoin(TxMsgTypeQueryTable, { TxCacheTable.id }, { TxMsgTypeQueryTable.txHashId })
152+
join = join.innerJoin(TxMsgTypeQueryTable, { TxCacheTable.id }, { txHashId })
155153
}
156154
if ((tqp.addressId != null && tqp.addressType != null) || tqp.address != null) {
157-
join = join.innerJoin(TxAddressJoinTable, { TxCacheTable.id }, { TxAddressJoinTable.txHashId })
155+
join = join.innerJoin(TxAddressJoinTable, { TxCacheTable.id }, { txHashId })
158156
}
159157
if (tqp.markerId != null || tqp.denom != null) {
160-
join = join.innerJoin(TxMarkerJoinTable, { TxCacheTable.id }, { TxMarkerJoinTable.txHashId })
158+
join = join.innerJoin(TxMarkerJoinTable, { TxCacheTable.id }, { txHashId })
161159
}
162160
if (tqp.nftId != null) {
163-
join = join.innerJoin(TxNftJoinTable, { TxCacheTable.id }, { TxNftJoinTable.txHashId })
161+
join = join.innerJoin(TxNftJoinTable, { TxCacheTable.id }, { txHashId })
164162
}
165163
if (tqp.ibcChannelIds.isNotEmpty()) {
166-
join = join.innerJoin(TxIbcTable, { TxCacheTable.id }, { TxIbcTable.txHashId })
164+
join = join.innerJoin(TxIbcTable, { TxCacheTable.id }, { txHashId })
167165
}
168166

169167
val query = if (distinctQuery != null) join.slice(distinctQuery).selectAll() else join.selectAll()
@@ -206,7 +204,79 @@ class TxCacheRecord(id: EntityID<Int>) : IntEntity(id) {
206204

207205
query
208206
}
209-
}
207+
208+
fun countForDates(daysPrior: Int): List<Pair<LocalDate, Long>> = transaction {
209+
val query = """
210+
select sum(daily_tx_cnt.cnt) as count, ds
211+
from (select count(*) cnt, tx_timestamp ts, date_trunc('day', tx_timestamp) ds
212+
from tx_cache
213+
where tx_timestamp > current_timestamp - interval '$daysPrior days'
214+
group by ts, ds) as daily_tx_cnt
215+
group by ds
216+
order by ds;
217+
""".trimIndent()
218+
query.execAndMap {
219+
Pair(
220+
it.getTimestamp("ds").toLocalDateTime().toLocalDate(),
221+
it.getLong("count")
222+
)
223+
}
224+
}
225+
226+
fun pulseTransactionsWithValue(denom: String, afterDateTime: LocalDateTime, page: Int, count: Int): PagedResults<Map<String, kotlin.Any?>> = transaction {
227+
val query = """
228+
select tx.id as tx_id,
229+
tx.hash,
230+
tx.height,
231+
tx.tx_timestamp,
232+
mtype.category,
233+
mtype.type,
234+
mtype.proto_type,
235+
mtype.module,
236+
tme.event_type,
237+
attr.attr_key,
238+
attr.attr_value
239+
from tx_cache tx
240+
join tx_msg_event as tme on tx.id = tme.tx_hash_id
241+
join tx_msg_event_attr as attr on tme.id = attr.tx_msg_event_id
242+
join tx_message_type as mtype on mtype.id = tme.tx_msg_type_id
243+
join tx_marker_join as denom on denom.tx_hash_id = tx.id
244+
where tme.tx_msg_type_id IN
245+
(select id from tx_message_type where module in ('exchange', 'bank'))
246+
and tx.tx_timestamp > ?
247+
and tx.error_code is null
248+
and tx.codespace is null
249+
and denom.denom = ?
250+
and event_type = 'coin_spent'
251+
and attr_key = 'amount'
252+
and attr_value like ?
253+
order by height desc, tx_id
254+
""".trimIndent()
255+
val arguments = mutableListOf<Pair<ColumnType, *>>(
256+
Pair(JavaLocalDateTimeColumnType(), afterDateTime),
257+
Pair(TextColumnType(), denom),
258+
Pair(TextColumnType(), "%$denom%"),
259+
)
260+
261+
val countQuery = "select count(*) from ($query) as count"
262+
val rowCount = countQuery.execAndMap(arguments) {
263+
it.getLong(1)
264+
}.first()
265+
266+
arguments.add(Pair(IntegerColumnType(), count))
267+
arguments.add(Pair(IntegerColumnType(), page * count))
268+
269+
"$query limit ? offset ?".execAndMap(arguments) {
270+
val map = mutableMapOf<String, kotlin.Any?>()
271+
(1..it.metaData.columnCount).forEach { index ->
272+
map[it.metaData.getColumnName(index)] = it.getObject(index)
273+
}
274+
map // return a list of map because i like to party
275+
}.let {
276+
PagedResults(rowCount.div(count).toInt(), it, rowCount, emptyMap())
277+
}
278+
}
279+
}
210280

211281
var hash by TxCacheTable.hash
212282
var height by TxCacheTable.height
@@ -364,7 +434,7 @@ class TxMessageRecord(id: EntityID<Int>) : IntEntity(id) {
364434

365435
fun findByHashIdPaginated(hashId: Int, msgTypes: List<Int>, limit: Int, offset: Int) = transaction {
366436
val query = TxMessageTable
367-
.innerJoin(TxMsgTypeSubtypeTable, { TxMessageTable.id }, { TxMsgTypeSubtypeTable.txMsgId })
437+
.innerJoin(TxMsgTypeSubtypeTable, { TxMessageTable.id }, { txMsgId })
368438
.slice(tableColSet)
369439
.select { TxMessageTable.txHashId eq hashId }
370440
if (msgTypes.isNotEmpty()) {
@@ -416,17 +486,17 @@ class TxMessageRecord(id: EntityID<Int>) : IntEntity(id) {
416486

417487
if (tqp.msgTypes.isNotEmpty())
418488
join = if (tqp.primaryTypesOnly)
419-
join.innerJoin(TxMsgTypeSubtypeTable, { TxMessageTable.txHashId }, { TxMsgTypeSubtypeTable.txHashId })
489+
join.innerJoin(TxMsgTypeSubtypeTable, { TxMessageTable.txHashId }, { txHashId })
420490
else
421-
join.innerJoin(TxMsgTypeQueryTable, { TxMessageTable.txHashId }, { TxMsgTypeQueryTable.txHashId })
491+
join.innerJoin(TxMsgTypeQueryTable, { TxMessageTable.txHashId }, { txHashId })
422492
if (tqp.txStatus != null)
423493
join = join.innerJoin(TxCacheTable, { TxMessageTable.txHashId }, { TxCacheTable.id })
424494
if ((tqp.addressId != null && tqp.addressType != null) || tqp.address != null)
425-
join = join.innerJoin(TxAddressJoinTable, { TxMessageTable.txHashId }, { TxAddressJoinTable.txHashId })
495+
join = join.innerJoin(TxAddressJoinTable, { TxMessageTable.txHashId }, { txHashId })
426496
if (tqp.smCodeId != null)
427-
join = join.innerJoin(TxSmCodeTable, { TxMessageTable.txHashId }, { TxSmCodeTable.txHashId })
497+
join = join.innerJoin(TxSmCodeTable, { TxMessageTable.txHashId }, { txHashId })
428498
if (tqp.smContractAddrId != null)
429-
join = join.innerJoin(TxSmContractTable, { TxMessageTable.txHashId }, { TxSmContractTable.txHashId })
499+
join = join.innerJoin(TxSmContractTable, { TxMessageTable.txHashId }, { txHashId })
430500

431501
val query = if (distinctQuery != null) join.slice(distinctQuery).selectAll() else join.selectAll()
432502

@@ -772,13 +842,13 @@ class TxFeeRecord(id: EntityID<Int>) : IntEntity(id) {
772842
}.let { (baseFeeOverage, baseFeeUsed) ->
773843
val nhash = assetService.getAssetRaw(ExplorerProperties.UTILITY_TOKEN).second
774844
// insert used fee
775-
feeList.add(buildInsert(txInfo, BASE_FEE_USED.name, nhash.id.value, nhash.denom, baseFeeUsed))
845+
feeList.add(buildInsert(txInfo, FeeType.BASE_FEE_USED.name, nhash.id.value, nhash.denom, baseFeeUsed))
776846
// insert paid too much fee if > 0
777847
if (baseFeeOverage > BigDecimal.ZERO) {
778848
feeList.add(
779849
buildInsert(
780850
txInfo,
781-
BASE_FEE_OVERAGE.name,
851+
FeeType.BASE_FEE_OVERAGE.name,
782852
nhash.id.value,
783853
nhash.denom,
784854
baseFeeOverage
@@ -789,7 +859,7 @@ class TxFeeRecord(id: EntityID<Int>) : IntEntity(id) {
789859
if (tx.success()) {
790860
msgBasedFeeList.forEach { fee ->
791861
val feeType =
792-
if (fee.msgType == CUSTOM_FEE_MSG_TYPE) CUSTOM_FEE.name else MSG_BASED_FEE.name
862+
if (fee.msgType == CUSTOM_FEE_MSG_TYPE) FeeType.CUSTOM_FEE.name else FeeType.MSG_BASED_FEE.name
793863
feeList.add(
794864
buildInsert(
795865
txInfo,

service/src/main/kotlin/io/provenance/explorer/domain/extensions/Extenstions.kt

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.provenance.explorer.config.ExplorerProperties.Companion.PROV_VAL_OPER_
1414
import io.provenance.explorer.domain.entities.MissedBlocksRecord
1515
import io.provenance.explorer.domain.exceptions.InvalidArgumentException
1616
import io.provenance.explorer.domain.models.explorer.Addresses
17+
import io.provenance.explorer.domain.models.explorer.pulse.MetricTrendType
1718
import io.provenance.explorer.model.base.Bech32
1819
import io.provenance.explorer.model.base.toBech32Data
1920
import io.provenance.explorer.model.base.toMAddress
@@ -181,3 +182,10 @@ fun List<BigDecimal>.average() = this.fold(BigDecimal.ZERO, BigDecimal::add)
181182
fun String.nullOrString() = this.ifBlank { null }
182183

183184
fun String.toNormalCase() = this.splitToWords().joinToString(" ")
185+
186+
fun BigDecimal.calculatePulseMetricTrend() =
187+
when {
188+
this > BigDecimal.ZERO -> MetricTrendType.UP
189+
this < BigDecimal.ZERO -> MetricTrendType.DOWN
190+
else -> MetricTrendType.FLAT
191+
}

0 commit comments

Comments
 (0)