From 51648e7a8ad4b65a4d064a3d6dc82c845fe123df Mon Sep 17 00:00:00 2001 From: Carolyn Russell Date: Tue, 7 Jun 2022 11:52:23 -0600 Subject: [PATCH] Saving applicable block heights for gov params (#363) * Allows us to pull param sets at height vs latest always * Separated out Proposal votes into its own paginated API * Added functionality to support these and future usecases * Fixing a couple bugs for IBC Recv and Gov Tx detail parsing closes: #341 closes: #362 --- CHANGELOG.md | 19 ++ .../V1_64__Add_gov_proposal_timing_table.sql | 145 +++++++++++++ .../explorer/config/SwaggerConfig.kt | 2 +- .../explorer/domain/core/sql/Array.kt | 1 + .../domain/core/sql/QueryFunctions.kt | 12 ++ .../explorer/domain/entities/Blocks.kt | 25 ++- .../explorer/domain/entities/Governance.kt | 69 +++++- .../explorer/domain/entities/Ibc.kt | 13 +- .../domain/extensions/CoinExtensions.kt | 5 +- .../domain/models/explorer/CommonModels.kt | 4 +- .../domain/models/explorer/GovModels.kt | 25 +++ .../models/explorer/TransactionModels.kt | 2 +- .../explorer/grpc/extensions/Domain.kt | 10 + .../explorer/grpc/v1/BankGrpcClient.kt | 4 +- .../explorer/grpc/v1/GovGrpcClient.kt | 55 +++-- .../explorer/service/AccountService.kt | 6 +- .../explorer/service/ExplorerService.kt | 16 +- .../provenance/explorer/service/GovService.kt | 196 +++++++++++++++--- .../explorer/service/NotificationService.kt | 3 +- .../explorer/service/ValidatorService.kt | 18 +- .../explorer/service/async/AsyncCachingV2.kt | 10 + .../explorer/web/v1/ExplorerController.kt | 118 ----------- .../explorer/web/v2/GeneralController.kt | 4 +- .../{GovController.kt => GovControllerV2.kt} | 5 +- .../explorer/web/v2/TransactionController.kt | 2 +- .../explorer/web/v3/GovControllerV3.kt | 37 ++++ 26 files changed, 592 insertions(+), 214 deletions(-) create mode 100644 database/src/main/resources/db/migration/V1_64__Add_gov_proposal_timing_table.sql delete mode 100644 service/src/main/kotlin/io/provenance/explorer/web/v1/ExplorerController.kt rename service/src/main/kotlin/io/provenance/explorer/web/v2/{GovController.kt => GovControllerV2.kt} (95%) create mode 100644 service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 318bdde1..8e83223e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,27 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### Features +* Separated out Proposal votes into a paginated API #362 + * Deprecated `/api/v2/gov/proposals/{id}/votes` in favor of `/api/v3/gov/proposals/{id}/votes` + * Moved Voting params and counts into the proposal detail / proposal listview responses + +### Improvements +* Now saving applicable block heights to pull governance param sets #341 + * This is used to populate past proposals/votes/deposits based on the contemporary parameters on chain +* Now have the ability to fetch data at height from the chain +* Added Exposed functionality for Arrays and ArrayAgg Postgres function + ### Bug Fixes * Fixed the Validator missed block count +* Added else case for IBC Recvs where the effected Recv is in the same tx as an uneffected Recv, which makes the block error out +* Added VotWeighted msg type to `getGovMsgDetail()` function + +### Data +* Migration 1.64 - Updates for gov params at height #341 + * Updated `gov_proposal` with `deposit_param_check_height`, `voting_param_check_height`, inserted data + * Updated `insert_gov_proposal()` procedure + * Created `get_last_block_before_timestamp()` function ## [v4.2.0](https://github.com/provenance-io/explorer-service/releases/tag/v4.2.0) - 2022-05-24 ### Release Name: Odoric of Pordenone diff --git a/database/src/main/resources/db/migration/V1_64__Add_gov_proposal_timing_table.sql b/database/src/main/resources/db/migration/V1_64__Add_gov_proposal_timing_table.sql new file mode 100644 index 00000000..2f29e7b6 --- /dev/null +++ b/database/src/main/resources/db/migration/V1_64__Add_gov_proposal_timing_table.sql @@ -0,0 +1,145 @@ +SELECT 'Updating gov_proposal table' AS comment; + +ALTER TABLE gov_proposal + ADD COLUMN IF NOT EXISTS deposit_param_check_height INT NOT NULL DEFAULT -1, + ADD COLUMN IF NOT EXISTS voting_param_check_height INT NOT NULL DEFAULT -1; + +CREATE INDEX IF NOT EXISTS block_timestamp_minute_idx ON block_cache (date_trunc('minute', block_timestamp)); + +SELECT 'Inserting new gov_proposal data columns' AS comment; +WITH base AS ( + SELECT proposal_id, + CASE + WHEN data ->> 'voting_start_time' = '0001-01-01T00:00:00Z' THEN (data ->> 'deposit_end_time')::timestamp + ELSE + (data ->> 'voting_start_time')::timestamp END AS vote_start, + data ->> 'voting_end_time' AS voting_end_str, + CASE + WHEN data ->> 'voting_end_time' = '0001-01-01T00:00:00Z' THEN null + ELSE + (data ->> 'voting_end_time')::timestamp END AS voting_end + FROM gov_proposal gp + ORDER BY proposal_id +), + deposit_blocks AS ( + SELECT height, + block_timestamp + FROM block_cache bc, + base + WHERE date_trunc('minute', bc.block_timestamp) = date_trunc('minute', base.vote_start) + -- allows us to capture the blocks on the frontside or backside of the minute interval + OR date_trunc('minute', bc.block_timestamp) = date_trunc('minute', base.vote_start) + INTERVAL '1 MINUTE' + OR date_trunc('minute', bc.block_timestamp) = date_trunc('minute', base.vote_start) - INTERVAL '1 MINUTE' + ), + deposit_match AS ( + SELECT base.proposal_id, + base.vote_start, + max(db.height) AS height + FROM deposit_blocks db, + base + WHERE block_timestamp <= base.vote_start + GROUP BY base.proposal_id, base.vote_start + ), + voting_blocks AS ( + SELECT height, + block_timestamp + FROM block_cache bc, + base + WHERE date_trunc('minute', bc.block_timestamp) = date_trunc('minute', base.voting_end) + -- allows us to capture the blocks on the frontside or backside of the minute interval + OR date_trunc('minute', bc.block_timestamp) = date_trunc('minute', base.voting_end) + INTERVAL '1 MINUTE' + OR date_trunc('minute', bc.block_timestamp) = date_trunc('minute', base.voting_end) - INTERVAL '1 MINUTE' + ), + voting_match AS ( + SELECT base.proposal_id, + base.voting_end, + max(vb.height) AS height + FROM voting_blocks vb, + base + WHERE base.voting_end IS NOT NULL + AND block_timestamp <= base.voting_end + GROUP BY base.proposal_id, base.voting_end + ) +UPDATE gov_proposal gp +SET deposit_param_check_height = q.deposit_height, + voting_param_check_height = q.voting_height +FROM ( + SELECT base.proposal_id, + base.vote_start AS deposit_param_height, + COALESCE(dm.height, -1) deposit_height, + base.voting_end AS voting_param_height, + COALESCE(vm.height, -1) voting_height + FROM base + LEFT JOIN deposit_match dm ON base.proposal_id = dm.proposal_id + LEFT JOIN voting_match vm ON base.proposal_id = vm.proposal_id + ) q +WHERE gp.proposal_id = q.proposal_id; + +SELECT 'Updating insert_gov_proposal() procedure' AS comment; +create or replace procedure insert_gov_proposal(proposals gov_proposal[], tx_height integer, tx_id integer, + timez timestamp without time zone) + language plpgsql as +$$ +DECLARE + gp gov_proposal; +BEGIN + FOREACH gp IN ARRAY proposals + LOOP + INSERT INTO gov_proposal(proposal_id, proposal_type, address_id, address, is_validator, title, description, + status, data, content, block_height, tx_hash, tx_timestamp, tx_hash_id, + deposit_param_check_height, voting_param_check_height) + VALUES (gp.proposal_id, + gp.proposal_type, + gp.address_id, + gp.address, + gp.is_validator, + gp.title, + gp.description, + gp.status, + gp.data, + gp.content, + tx_height, + gp.tx_hash, + timez, + tx_id, + gp.deposit_param_check_height, + gp.voting_param_check_height) + ON CONFLICT (proposal_id) DO UPDATE + SET status = gp.status, + data = gp.data, + tx_hash = gp.tx_hash, + tx_timestamp = timez, + block_height = tx_height, + deposit_param_check_height = gp.deposit_param_check_height, + voting_param_check_height = gp.voting_param_check_height; + END LOOP; +END; +$$; + +SELECT 'Creating get_last_block_before_timestamp() function' AS comment; +CREATE OR REPLACE FUNCTION get_last_block_before_timestamp(input timestamp without time zone DEFAULT NULL::timestamp without time zone) + RETURNS integer + LANGUAGE plpgsql +AS +$$ +BEGIN + IF (input IS NULL) THEN + RETURN -1; + ELSE + RETURN ( + WITH voting_blocks AS ( + SELECT height, block_timestamp + FROM block_cache bc + WHERE date_trunc('minute', bc.block_timestamp) = date_trunc('minute', input) + -- allows us to capture the blocks on the frontside or backside of the minute interval + OR date_trunc('minute', bc.block_timestamp) = date_trunc('minute', input) + INTERVAL '1 MINUTE' + OR date_trunc('minute', bc.block_timestamp) = date_trunc('minute', input) - INTERVAL '1 MINUTE') + SELECT COALESCE(max(vb.height), -1) AS height + FROM voting_blocks vb + WHERE input IS NOT NULL + AND block_timestamp <= input + LIMIT 1 + ); + END IF; +END +$$; diff --git a/service/src/main/kotlin/io/provenance/explorer/config/SwaggerConfig.kt b/service/src/main/kotlin/io/provenance/explorer/config/SwaggerConfig.kt index ffd32546..3db10904 100644 --- a/service/src/main/kotlin/io/provenance/explorer/config/SwaggerConfig.kt +++ b/service/src/main/kotlin/io/provenance/explorer/config/SwaggerConfig.kt @@ -42,7 +42,7 @@ class SwaggerConfig(val props: ExplorerProperties) { .forCodeGeneration(true) .securitySchemes(listOf()) .select() - .apis(RequestHandlerSelectors.basePackage("io.provenance.explorer.web.v2")) + .apis(RequestHandlerSelectors.basePackage("io.provenance.explorer.web")) if (props.hiddenApis()) docket.apis(Predicate.not(RequestHandlerSelectors.withClassAnnotation(HiddenApi::class.java))) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/Array.kt b/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/Array.kt index 3b8d4978..83121b7c 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/Array.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/Array.kt @@ -13,6 +13,7 @@ import org.jetbrains.exposed.sql.QueryParameter import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.statements.jdbc.JdbcConnectionImpl import org.jetbrains.exposed.sql.transactions.TransactionManager +import kotlin.Array fun Table.array(name: String, columnType: ColumnType): Column> = registerColumn(name, ArrayColumnType(columnType)) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/QueryFunctions.kt b/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/QueryFunctions.kt index 09e2d6c4..15f69310 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/QueryFunctions.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/core/sql/QueryFunctions.kt @@ -7,6 +7,7 @@ import org.jetbrains.exposed.sql.Function import org.jetbrains.exposed.sql.IColumnType import org.jetbrains.exposed.sql.IntegerColumnType import org.jetbrains.exposed.sql.QueryBuilder +import org.jetbrains.exposed.sql.TextColumnType import org.jetbrains.exposed.sql.VarCharColumnType import org.jetbrains.exposed.sql.append import org.jetbrains.exposed.sql.jodatime.CustomDateTimeFunction @@ -14,6 +15,7 @@ import org.jetbrains.exposed.sql.jodatime.DateColumnType import org.jetbrains.exposed.sql.stringLiteral import org.joda.time.DateTime import java.math.BigDecimal +import kotlin.Array // Generic Distinct function class Distinct(val expr: Expression, _columnType: IColumnType) : Function(_columnType) { @@ -68,3 +70,13 @@ class ColumnNullsLast(private val col: Expression<*>) : Expression() { fun Expression<*>.nullsLast() = ColumnNullsLast(this) fun EntityID?.getOrNull() = try { this?.value } catch (e: Exception) { null } + +class Array(val exprs: List>) : Function>(ArrayColumnType(TextColumnType())) { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("ARRAY [", exprs.joinToString(","), "]") } +} + +fun List>.joinToList() = this.joinToString(",") { "$it::text" } + +class ArrayAgg(val expr: Expression>) : Function>>(ArrayColumnType(ArrayColumnType(TextColumnType()))) { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("array_agg(", expr, ")") } +} diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Blocks.kt b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Blocks.kt index 6f9fd30e..fb40cc81 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Blocks.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Blocks.kt @@ -11,6 +11,7 @@ import io.provenance.explorer.domain.core.sql.jsonb import io.provenance.explorer.domain.core.sql.toProcedureObject import io.provenance.explorer.domain.extensions.average import io.provenance.explorer.domain.extensions.exec +import io.provenance.explorer.domain.extensions.execAndMap import io.provenance.explorer.domain.extensions.map import io.provenance.explorer.domain.extensions.startOfDay import io.provenance.explorer.domain.models.explorer.BlockProposer @@ -18,6 +19,7 @@ import io.provenance.explorer.domain.models.explorer.BlockUpdate import io.provenance.explorer.domain.models.explorer.DateTruncGranularity import io.provenance.explorer.domain.models.explorer.DateTruncGranularity.DAY import io.provenance.explorer.domain.models.explorer.DateTruncGranularity.HOUR +import io.provenance.explorer.domain.models.explorer.DateTruncGranularity.MINUTE import io.provenance.explorer.domain.models.explorer.MissedBlockPeriod import io.provenance.explorer.domain.models.explorer.TxHeatmap import io.provenance.explorer.domain.models.explorer.TxHeatmapDay @@ -84,7 +86,7 @@ class BlockCacheRecord(id: EntityID) : CacheEntity(id) { fun getDaysBetweenHeights(minHeight: Int, maxHeight: Int) = transaction { val dateTrunc = Distinct( - DateTrunc(DateTruncGranularity.DAY.name, BlockCacheTable.blockTimestamp), + DateTrunc(DAY.name, BlockCacheTable.blockTimestamp), DateColumnType(true) ).count() BlockCacheTable.slice(dateTrunc) @@ -92,6 +94,9 @@ class BlockCacheRecord(id: EntityID) : CacheEntity(id) { .first()[dateTrunc].toInt() } + fun test() = transaction { + } + fun getMaxBlockHeightOrNull() = transaction { val maxHeight = Max(BlockCacheTable.height, IntegerColumnType()) BlockCacheTable.slice(maxHeight).selectAll().firstOrNull()?.let { it[maxHeight] } @@ -107,6 +112,12 @@ class BlockCacheRecord(id: EntityID) : CacheEntity(id) { .orderBy(BlockCacheTable.height to SortOrder.ASC) .first() } + + fun getLastBlockBeforeTime(time: DateTime?) = transaction { + val query = "SELECT get_last_block_before_timestamp(?);" + val arguments = listOf(Pair(DateColumnType(true), time)) + query.execAndMap(arguments) { it.getInt("get_last_block_before_timestamp") }.first() + } } var height by BlockCacheTable.height @@ -333,6 +344,7 @@ class BlockCacheHourlyTxCountsRecord(id: EntityID) : Entity( when (granularity) { HOUR -> getHourlyCounts(fromDate, toDate) DAY -> getDailyCounts(fromDate, toDate) + MINUTE -> emptyList() } } @@ -382,7 +394,7 @@ class BlockCacheHourlyTxCountsRecord(id: EntityID) : Entity( } private fun getDailyCounts(fromDate: DateTime, toDate: DateTime) = transaction { - val dateTrunc = DateTrunc("DAY", BlockCacheHourlyTxCountsTable.blockTimestamp) + val dateTrunc = DateTrunc(DAY.name, BlockCacheHourlyTxCountsTable.blockTimestamp) val txSum = BlockCacheHourlyTxCountsTable.txCount.sum() BlockCacheHourlyTxCountsTable.slice(dateTrunc, txSum) .select { @@ -480,6 +492,15 @@ class BlockTxRetryRecord(id: EntityID) : IntEntity(id) { } } + fun insertNonBlockingRetry(height: Int, e: Exception) = transaction { + BlockTxRetryTable.insertIgnore { + it[this.height] = height + it[this.errorBlock] = + "NON BLOCKING ERROR: Logged to know what happened, but didnt stop processing.\n " + + e.stackTraceToString() + } + } + fun getRecordsToRetry() = transaction { BlockTxRetryRecord .find { (BlockTxRetryTable.retried eq false) and (BlockTxRetryTable.success eq false) } diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt index c0f5f318..0a9e61b5 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt @@ -6,19 +6,26 @@ import com.google.protobuf.util.JsonFormat import cosmos.base.v1beta1.CoinOuterClass import cosmos.gov.v1beta1.Gov import io.provenance.explorer.OBJECT_MAPPER +import io.provenance.explorer.domain.core.sql.Array +import io.provenance.explorer.domain.core.sql.ArrayAgg import io.provenance.explorer.domain.core.sql.jsonb import io.provenance.explorer.domain.core.sql.toProcedureObject import io.provenance.explorer.domain.extensions.toDecimal import io.provenance.explorer.domain.models.explorer.GovAddrData +import io.provenance.explorer.domain.models.explorer.ProposalParamHeights import io.provenance.explorer.domain.models.explorer.TxData import io.provenance.explorer.domain.models.explorer.VoteDbRecord +import io.provenance.explorer.domain.models.explorer.VoteDbRecordAgg +import io.provenance.explorer.domain.models.explorer.VoteWeightDbObj import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.TextColumnType import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.castTo import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.jodatime.datetime import org.jetbrains.exposed.sql.select @@ -40,6 +47,8 @@ object GovProposalTable : IntIdTable(name = "gov_proposal") { val txHash = varchar("tx_hash", 64) val txTimestamp = datetime("tx_timestamp") val txHashId = reference("tx_hash_id", TxCacheTable) + val depositParamCheckHeight = integer("deposit_param_check_height") + val votingParamCheckHeight = integer("voting_param_check_height") } fun String.getProposalType() = this.split(".").last().replace("Proposal", "") @@ -96,7 +105,8 @@ class GovProposalRecord(id: EntityID) : IntEntity(id) { protoPrinter: JsonFormat.Printer, txInfo: TxData, addrInfo: GovAddrData, - isSubmit: Boolean + isSubmit: Boolean, + paramHeights: ProposalParamHeights ) = transaction { proposal.content.toProposalTitleAndDescription(protoPrinter).let { (title, description) -> val (hash, block, time) = findByProposalId(proposal.proposalId)?.let { @@ -118,7 +128,9 @@ class GovProposalRecord(id: EntityID) : IntEntity(id) { block, hash, time, - 0 + 0, + paramHeights.depositCheckHeight, + paramHeights.votingCheckHeight ).toProcedureObject() } } @@ -138,6 +150,8 @@ class GovProposalRecord(id: EntityID) : IntEntity(id) { var txHash by GovProposalTable.txHash var txTimestamp by GovProposalTable.txTimestamp var txHashId by TxCacheRecord referencedOn GovProposalTable.txHashId + var depositParamCheckHeight by GovProposalTable.depositParamCheckHeight + var votingParamCheckHeight by GovProposalTable.votingParamCheckHeight } object GovVoteTable : IntIdTable(name = "gov_vote") { @@ -153,6 +167,9 @@ object GovVoteTable : IntIdTable(name = "gov_vote") { val txHashId = reference("tx_hash_id", TxCacheTable) } +fun kotlin.Array>.toVoteWeightObj() = + this.map { VoteWeightDbObj(it[0], it[1].toDouble()) }.toList() + class GovVoteRecord(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(GovVoteTable) { @@ -175,6 +192,54 @@ class GovVoteRecord(id: EntityID) : IntEntity(id) { } } + fun findByProposalIdPaginated(proposalId: Long, limit: Int, offset: Int) = transaction { + val array = Array(listOf(GovVoteTable.vote.castTo(TextColumnType()), GovVoteTable.weight.castTo(TextColumnType()))) + val arrayAgg = ArrayAgg(array) + GovVoteTable.slice( + listOf( + GovVoteTable.proposalId, + GovVoteTable.address, + GovVoteTable.isValidator, + arrayAgg, + GovVoteTable.blockHeight, + GovVoteTable.txHash, + GovVoteTable.txTimestamp + ) + ).select { GovVoteTable.proposalId eq proposalId } + .groupBy( + GovVoteTable.proposalId, + GovVoteTable.address, + GovVoteTable.isValidator, + GovVoteTable.blockHeight, + GovVoteTable.txHash, + GovVoteTable.txTimestamp + ) + .orderBy(Pair(GovVoteTable.blockHeight, SortOrder.DESC)) + .limit(limit, offset.toLong()) + .map { + VoteDbRecordAgg( + it[GovVoteTable.address], + it[GovVoteTable.isValidator], + it[arrayAgg].toVoteWeightObj(), + it[GovVoteTable.blockHeight], + it[GovVoteTable.txHash], + it[GovVoteTable.txTimestamp], + it[GovVoteTable.proposalId], + "", + "" + ) + } + } + + fun findByProposalIdCount(proposalId: Long) = transaction { + val array = Array(listOf(GovVoteTable.vote.castTo(TextColumnType()), GovVoteTable.weight.castTo(TextColumnType()))) + val arrayAgg = ArrayAgg(array) + GovVoteTable.slice(listOf(GovVoteTable.proposalId, GovVoteTable.address, arrayAgg)) + .select { GovVoteTable.proposalId eq proposalId } + .groupBy(GovVoteTable.proposalId, GovVoteTable.address) + .count() + } + fun getByAddrIdPaginated(addrId: Int, limit: Int, offset: Int) = transaction { val proposalIds = GovVoteTable.slice(GovVoteTable.proposalId) .select { GovVoteTable.addressId eq addrId } diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Ibc.kt b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Ibc.kt index ed7eecba..a5d0c484 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Ibc.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Ibc.kt @@ -180,15 +180,16 @@ class IbcLedgerRecord(id: EntityID) : IntEntity(id) { else match.ackSuccess else ledger.ackSuccess, match?.sequence ?: ledger.sequence, - match?.uniqueHash - ?: listOf( - ledger.channel!!.id.value, - ledger.sequence, - if (ledger.movementIn) IbcMovementType.IN.name else IbcMovementType.OUT.name - ).joinToString("").toDbHash() + match?.uniqueHash ?: getUniqueHash(ledger) ).toProcedureObject() } + fun getUniqueHash(ledger: LedgerInfo) = listOf( + ledger.channel!!.id.value, + ledger.sequence, + if (ledger.movementIn) IbcMovementType.IN.name else IbcMovementType.OUT.name + ).joinToString("").toDbHash() + val lastTxTime = Max(IbcLedgerTable.txTimestamp, DateColumnType(true)) val balanceInSum = Sum(IbcLedgerTable.balanceIn, DecimalColumnType(100, 10)) val balanceOutSum = Sum(IbcLedgerTable.balanceOut, DecimalColumnType(100, 10)) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt index db30a803..a5648996 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt @@ -13,7 +13,10 @@ fun BigDecimal.stringfy() = this.stripTrailingZeros().toPlainString() fun BigDecimal.toCoinStr(denom: String) = CoinStr(this.stringfy(), denom) -fun String.toDecCoin() = this.toDecimal().toPlainString() +fun String.toDecimalString() = this.toDecimal().toPlainString() + +// Used to convert voting power values from mhash (milli) to nhash (nano) +fun Long.mhashToNhash() = this * 1000000 fun List.toProtoCoin() = this.firstOrNull() ?: CoinOuterClass.Coin.newBuilder().setAmount("0").setDenom("").build() diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/CommonModels.kt b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/CommonModels.kt index e1f0df7e..52821e94 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/CommonModels.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/CommonModels.kt @@ -4,7 +4,7 @@ import cosmos.base.v1beta1.CoinOuterClass import io.provenance.explorer.domain.entities.MarkerCacheRecord import io.provenance.explorer.domain.extensions.USD_UPPER import io.provenance.explorer.domain.extensions.toCoinStr -import io.provenance.explorer.domain.extensions.toDecCoin +import io.provenance.explorer.domain.extensions.toDecimalString import java.math.BigDecimal import java.math.BigInteger @@ -57,7 +57,7 @@ fun MarkerCacheRecord.toCoinStrWithPrice(price: BigDecimal?) = this.supply.toCoinStrWithPrice(price, this.denom) fun CoinOuterClass.DecCoin.toCoinStrWithPrice(price: BigDecimal?) = - this.amount.toDecCoin().toBigDecimal().toCoinStrWithPrice(price, this.denom) + this.amount.toDecimalString().toBigDecimal().toCoinStrWithPrice(price, this.denom) data class CountTotal( val count: BigInteger?, diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/GovModels.kt b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/GovModels.kt index 6cf2e8e0..acb57a43 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/GovModels.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/GovModels.kt @@ -46,11 +46,17 @@ data class GovAddress( data class ProposalTimings( val deposit: DepositPercentage, + val voting: VotingDetails, val submitTime: String, val depositEndTime: String, val votingTime: GovTimeFrame ) +data class VotingDetails( + val params: TallyParams, + val tally: VotesTally +) + data class GovVotesDetail( val params: TallyParams, val tally: VotesTally, @@ -101,6 +107,18 @@ data class VoteDbRecord( val proposalStatus: String ) +data class VoteDbRecordAgg( + val voter: String, + val isValidator: Boolean, + val voteWeight: List, + val blockHeight: Int, + val txHash: String, + val txTimestamp: DateTime, + val proposalId: Long, + val proposalTitle: String, + val proposalStatus: String +) + data class DepositRecord( val voter: GovAddress, val type: String, @@ -116,3 +134,10 @@ data class GovMsgDetail( val proposalId: Long, var proposalTitle: String ) + +data class ProposalParamHeights( + val depositCheckHeight: Int, + val votingCheckHeight: Int +) + +data class VoteWeightDbObj(val vote: String, val weight: Double) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/TransactionModels.kt b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/TransactionModels.kt index 12f4ef86..873c8a83 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/TransactionModels.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/TransactionModels.kt @@ -257,7 +257,7 @@ data class TxMessage( val msg: ObjectNode ) -enum class DateTruncGranularity { DAY, HOUR } +enum class DateTruncGranularity { DAY, HOUR, MINUTE } enum class TxStatus { SUCCESS, FAILURE } diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt index 633811eb..07346048 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt @@ -9,6 +9,9 @@ import cosmos.gov.v1beta1.Gov import cosmos.gov.v1beta1.Tx import cosmos.mint.v1beta1.Mint import cosmos.slashing.v1beta1.Slashing +import io.grpc.Metadata +import io.grpc.stub.AbstractStub +import io.grpc.stub.MetadataUtils import io.provenance.explorer.config.ResourceNotFoundException import io.provenance.explorer.domain.core.logger import io.provenance.explorer.domain.core.toBech32Data @@ -29,6 +32,13 @@ import io.provenance.marker.v1.MarkerAccount import io.provenance.marker.v1.MarkerStatus import io.provenance.msgfees.v1.MsgFee +const val BLOCK_HEIGHT = "x-cosmos-block-height" + +fun > S.addBlockHeightToQuery(blockHeight: String): S = + Metadata() + .also { it.put(Metadata.Key.of(BLOCK_HEIGHT, Metadata.ASCII_STRING_MARSHALLER), blockHeight) } + .let { MetadataUtils.attachHeaders(this, it) } + // Marker Extensions fun String.getTypeShortName() = this.split(".").last() diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt index 59b99d8a..a47e5b1b 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt @@ -3,7 +3,7 @@ package io.provenance.explorer.grpc.v1 import io.grpc.ManagedChannelBuilder import io.provenance.explorer.config.GrpcLoggingInterceptor import io.provenance.explorer.domain.core.logger -import io.provenance.explorer.domain.extensions.toDecCoin +import io.provenance.explorer.domain.extensions.toDecimalString import org.springframework.stereotype.Component import java.net.URI import java.util.concurrent.TimeUnit @@ -49,7 +49,7 @@ class BankGrpcClient(channelUri: URI) { fun getCommunityPoolAmount(denom: String): String = distClient.communityPool(DistOuterClass.QueryCommunityPoolRequest.newBuilder().build()).poolList - .filter { it.denom == denom }[0]?.amount!!.toDecCoin() + .filter { it.denom == denom }[0]?.amount!!.toDecimalString() fun getStakingPool() = stakingClient.pool(StakingOuterClass.QueryPoolRequest.getDefaultInstance()) diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt index 146c6ab5..31f82f30 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt @@ -1,19 +1,25 @@ package io.provenance.explorer.grpc.v1 -import cosmos.gov.v1beta1.QueryGrpc -import cosmos.gov.v1beta1.QueryOuterClass +import cosmos.gov.v1beta1.queryParamsRequest +import cosmos.gov.v1beta1.queryProposalRequest +import cosmos.gov.v1beta1.queryTallyResultRequest +import cosmos.upgrade.v1beta1.queryAppliedPlanRequest +import cosmos.upgrade.v1beta1.queryCurrentPlanRequest import io.grpc.ManagedChannelBuilder import io.provenance.explorer.config.GrpcLoggingInterceptor import io.provenance.explorer.domain.models.explorer.GovParamType +import io.provenance.explorer.grpc.extensions.addBlockHeightToQuery import org.springframework.stereotype.Component import java.net.URI import java.util.concurrent.TimeUnit +import cosmos.gov.v1beta1.QueryGrpcKt as GovQueryGrpc +import cosmos.upgrade.v1beta1.QueryGrpcKt as UpgradeQueryGrpc @Component class GovGrpcClient(channelUri: URI) { - private val govClient: QueryGrpc.QueryBlockingStub - private val upgradeClient: cosmos.upgrade.v1beta1.QueryGrpc.QueryBlockingStub + private val govClient: GovQueryGrpc.QueryCoroutineStub + private val upgradeClient: UpgradeQueryGrpc.QueryCoroutineStub init { val channel = @@ -31,37 +37,46 @@ class GovGrpcClient(channelUri: URI) { .intercept(GrpcLoggingInterceptor()) .build() - govClient = QueryGrpc.newBlockingStub(channel) - upgradeClient = cosmos.upgrade.v1beta1.QueryGrpc.newBlockingStub(channel) + govClient = GovQueryGrpc.QueryCoroutineStub(channel) + upgradeClient = UpgradeQueryGrpc.QueryCoroutineStub(channel) } - fun getProposal(proposalId: Long) = + suspend fun getProposal(proposalId: Long) = try { - govClient.proposal(QueryOuterClass.QueryProposalRequest.newBuilder().setProposalId(proposalId).build()) + govClient.proposal(queryProposalRequest { this.proposalId = proposalId }) } catch (e: Exception) { null } - fun getParams(param: GovParamType) = - govClient.params(QueryOuterClass.QueryParamsRequest.newBuilder().setParamsType(param.name).build()) + suspend fun getParams(param: GovParamType) = + govClient.params(queryParamsRequest { this.paramsType = param.name }) - fun getTally(proposalId: Long) = - govClient.tallyResult(QueryOuterClass.QueryTallyResultRequest.newBuilder().setProposalId(proposalId).build()) + suspend fun getParamsAtHeight(param: GovParamType, height: Int) = + try { + govClient + .addBlockHeightToQuery(height.toString()) + .params(queryParamsRequest { this.paramsType = param.name }) + } catch (e: Exception) { + null + } + + suspend fun getTally(proposalId: Long) = + try { + govClient.tallyResult(queryTallyResultRequest { this.proposalId = proposalId }) + } catch (e: Exception) { + null + } - fun getIfUpgradeApplied(planName: String) = + suspend fun getIfUpgradeApplied(planName: String) = try { - upgradeClient.appliedPlan( - cosmos.upgrade.v1beta1.QueryOuterClass.QueryAppliedPlanRequest.newBuilder() - .setName(planName) - .build() - ) + upgradeClient.appliedPlan(queryAppliedPlanRequest { this.name = planName }) } catch (e: Exception) { null } - fun getIfUpgradeScheduled() = + suspend fun getIfUpgradeScheduled() = try { - upgradeClient.currentPlan(cosmos.upgrade.v1beta1.QueryOuterClass.QueryCurrentPlanRequest.newBuilder().build()) + upgradeClient.currentPlan(queryCurrentPlanRequest { }) } catch (e: Exception) { null } diff --git a/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt b/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt index 66036682..9b682996 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt @@ -14,7 +14,7 @@ import io.provenance.explorer.domain.extensions.toAccountPubKey import io.provenance.explorer.domain.extensions.toBase64 import io.provenance.explorer.domain.extensions.toCoinStr import io.provenance.explorer.domain.extensions.toDateTime -import io.provenance.explorer.domain.extensions.toDecCoin +import io.provenance.explorer.domain.extensions.toDecimalString import io.provenance.explorer.domain.extensions.toObjectNode import io.provenance.explorer.domain.extensions.toOffset import io.provenance.explorer.domain.models.explorer.AccountDetail @@ -128,7 +128,7 @@ class AccountService( null, CoinStr(it.balance.amount, it.balance.denom), null, - it.delegation.shares.toDecCoin(), + it.delegation.shares.toDecimalString(), null, null ) @@ -170,7 +170,7 @@ class AccountService( list.redelegation.validatorDstAddress, CoinStr(it.balance, NHASH), CoinStr(it.redelegationEntry.initialBalance, NHASH), - it.redelegationEntry.sharesDst.toDecCoin(), + it.redelegationEntry.sharesDst.toDecimalString(), it.redelegationEntry.creationHeight.toInt(), it.redelegationEntry.completionTime.toDateTime() ) diff --git a/service/src/main/kotlin/io/provenance/explorer/service/ExplorerService.kt b/service/src/main/kotlin/io/provenance/explorer/service/ExplorerService.kt index bc67d3df..5d96ee8a 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/ExplorerService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/ExplorerService.kt @@ -198,7 +198,7 @@ class ExplorerService( fun getChainUpgrades(): List { val typeUrl = govService.getUpgradeProtoType() - val scheduledName = govClient.getIfUpgradeScheduled()?.plan?.name + val scheduledName = runBlocking { govClient.getIfUpgradeScheduled()?.plan?.name } val proposals = GovProposalRecord.findByProposalType(typeUrl) .filter { it.status == Gov.ProposalStatus.PROPOSAL_STATUS_PASSED.name } val knownReleases = @@ -230,7 +230,7 @@ class ExplorerService( one.content.get("plan").get("name").asText(), one.content.get("plan").get("info").asText().getChainVersionFromUrl(props.upgradeVersionRegex), version, - govClient.getIfUpgradeApplied(one.content.get("plan").get("name").asText()) + runBlocking { govClient.getIfUpgradeApplied(one.content.get("plan").get("name").asText()) } ?.let { it.height.toInt() != one.content.get("plan").get("height").asInt() } ?: true, scheduledName?.let { name -> name == one.content.get("plan").get("name").asText() } ?: false, @@ -306,9 +306,9 @@ class ExplorerService( val authParams = async { accountClient.getAuthParams().params } val bankParams = async { accountClient.getBankParams().params } val distParams = validatorClient.getDistParams().params - val votingParams = govClient.getParams(GovParamType.voting).votingParams - val tallyParams = govClient.getParams(GovParamType.tallying).tallyParams - val depositParams = govClient.getParams(GovParamType.deposit).depositParams + val votingParams = async { govClient.getParams(GovParamType.voting).votingParams } + val tallyParams = async { govClient.getParams(GovParamType.tallying).tallyParams } + val depositParams = async { govClient.getParams(GovParamType.deposit).depositParams } val mintParams = async { accountClient.getMintParams().params } val slashingParams = validatorClient.getSlashingParams().params val stakingParams = validatorClient.getStakingParams().params @@ -327,9 +327,9 @@ class ExplorerService( .toObjectNodePrint(protoPrinter), distParams.toDto(), GovParams( - votingParams.toObjectNodePrint(protoPrinter), - tallyParams.toDto(), - depositParams.toObjectNodePrint(protoPrinter), + votingParams.await().toObjectNodePrint(protoPrinter), + tallyParams.await().toDto(), + depositParams.await().toObjectNodePrint(protoPrinter), ), mintParams.await().toDto(), slashingParams.toDto(), diff --git a/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt b/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt index f07ac795..c3a8571d 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt @@ -19,17 +19,17 @@ import io.provenance.explorer.domain.entities.ProposalMonitorRecord import io.provenance.explorer.domain.entities.ProposalType import io.provenance.explorer.domain.entities.SmCodeRecord import io.provenance.explorer.domain.entities.TxSmCodeRecord -import io.provenance.explorer.domain.entities.ValidatorState.ACTIVE import io.provenance.explorer.domain.entities.ValidatorStateRecord import io.provenance.explorer.domain.extensions.NHASH import io.provenance.explorer.domain.extensions.formattedString +import io.provenance.explorer.domain.extensions.mhashToNhash import io.provenance.explorer.domain.extensions.pageCountOfResults import io.provenance.explorer.domain.extensions.stringfy import io.provenance.explorer.domain.extensions.to256Hash import io.provenance.explorer.domain.extensions.toBase64 import io.provenance.explorer.domain.extensions.toCoinStr import io.provenance.explorer.domain.extensions.toDateTime -import io.provenance.explorer.domain.extensions.toDecCoin +import io.provenance.explorer.domain.extensions.toDecimalString import io.provenance.explorer.domain.extensions.toOffset import io.provenance.explorer.domain.models.explorer.CoinStr import io.provenance.explorer.domain.models.explorer.DepositPercentage @@ -43,18 +43,23 @@ import io.provenance.explorer.domain.models.explorer.GovTimeFrame import io.provenance.explorer.domain.models.explorer.GovVotesDetail import io.provenance.explorer.domain.models.explorer.PagedResults import io.provenance.explorer.domain.models.explorer.ProposalHeader +import io.provenance.explorer.domain.models.explorer.ProposalParamHeights import io.provenance.explorer.domain.models.explorer.ProposalTimings import io.provenance.explorer.domain.models.explorer.Tally import io.provenance.explorer.domain.models.explorer.TallyParams import io.provenance.explorer.domain.models.explorer.TxData import io.provenance.explorer.domain.models.explorer.VoteDbRecord +import io.provenance.explorer.domain.models.explorer.VoteDbRecordAgg import io.provenance.explorer.domain.models.explorer.VoteRecord import io.provenance.explorer.domain.models.explorer.VotesTally +import io.provenance.explorer.domain.models.explorer.VotingDetails import io.provenance.explorer.domain.models.explorer.toData import io.provenance.explorer.grpc.extensions.toMsgDeposit import io.provenance.explorer.grpc.extensions.toMsgVote +import io.provenance.explorer.grpc.extensions.toMsgVoteWeighted import io.provenance.explorer.grpc.v1.GovGrpcClient import io.provenance.explorer.grpc.v1.SmartContractGrpcClient +import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.stereotype.Service import java.math.BigDecimal @@ -69,25 +74,50 @@ class GovService( ) { protected val logger = logger(GovService::class) - fun buildProposal(proposalId: Long, txInfo: TxData, addr: String, isSubmit: Boolean) = transaction { + fun buildProposal(proposalId: Long, txInfo: TxData, addr: String, isSubmit: Boolean) = runBlocking { govClient.getProposal(proposalId)?.let { - GovProposalRecord.buildInsert(it.proposal, protoPrinter, txInfo, getAddressDetails(addr), isSubmit) + GovProposalRecord.buildInsert( + it.proposal, + protoPrinter, + txInfo, + getAddressDetails(addr), + isSubmit, + getParamHeights(it.proposal) + ) } } + fun getParamHeights(proposal: Gov.Proposal) = + proposal.let { prop -> + val depositTime = if (prop.votingStartTime.toString() == "0001-01-01T00:00:00Z") prop.depositEndTime + else prop.votingStartTime + val votingTime = if (prop.votingEndTime.toString() == "0001-01-01T00:00:00Z") null else prop.votingEndTime + + ProposalParamHeights( + BlockCacheRecord.getLastBlockBeforeTime(depositTime.toDateTime()), + BlockCacheRecord.getLastBlockBeforeTime(votingTime?.toDateTime()) + ) + } + fun updateProposal(record: GovProposalRecord) = transaction { - govClient.getProposal(record.proposalId)?.let { res -> - if (res.proposal.status.name != record.status) - record.apply { - this.status = res.proposal.status.name - this.data = res.proposal + runBlocking { + govClient.getProposal(record.proposalId)?.let { res -> + if (res.proposal.status.name != record.status) { + val paramHeights = getParamHeights(res.proposal) + record.apply { + this.status = res.proposal.status.name + this.data = res.proposal + this.depositParamCheckHeight = paramHeights.depositCheckHeight + this.votingParamCheckHeight = paramHeights.votingCheckHeight + } } - } // Assumes it is gone due to no deposit - ?: record.apply { this.status = Gov.ProposalStatus.PROPOSAL_STATUS_REJECTED.name } + } // Assumes it is gone due to no deposit + ?: record.apply { this.status = Gov.ProposalStatus.PROPOSAL_STATUS_REJECTED.name } + } } - fun buildProposalMonitor(txMsg: Tx.MsgSubmitProposal, proposalId: Long, txInfo: TxData) = transaction { - val proposal = govClient.getProposal(proposalId) ?: return@transaction null + fun buildProposalMonitor(txMsg: Tx.MsgSubmitProposal, proposalId: Long, txInfo: TxData) = runBlocking { + val proposal = govClient.getProposal(proposalId) ?: return@runBlocking null val (proposalType, dataHash) = when { txMsg.content.typeUrl.endsWith("v1beta1.StoreCodeProposal") -> ProposalType.STORE_CODE to @@ -99,7 +129,7 @@ class GovService( // base64(sha256(gzipUncompress(wasmByteCode))) == base64(storedCode.data_hash) txMsg.content.unpack(cosmwasm.wasm.v1.Proposal.StoreCodeProposal::class.java) .wasmByteCode.gzipUncompress().to256Hash() - else -> return@transaction null + else -> return@runBlocking null } val votingEndTime = proposal.proposal.votingEndTime.toDateTime() val avgBlockTime = cacheService.getAvgBlockTime().multiply(BigDecimal(1000)).toLong() @@ -118,7 +148,7 @@ class GovService( fun processProposal(proposalMon: ProposalMonitorRecord) = transaction { when (ProposalType.valueOf(proposalMon.proposalType)) { ProposalType.STORE_CODE -> { - val creationHeight = BlockCacheRecord.getFirstBlockAfterTime(proposalMon.votingEndTime).height + val creationHeight = BlockCacheRecord.getLastBlockBeforeTime(proposalMon.votingEndTime) + 1 val records = SmCodeRecord.all().sortedByDescending { it.id.value } val matching = records.firstOrNull { proposalMon.dataHash == it.dataHash } // find existing record and update, else search for next code id only. @@ -175,9 +205,12 @@ class GovService( fun buildVote(txInfo: TxData, votes: List, voter: String, proposalId: Long) = GovVoteRecord.buildInsert(txInfo, votes, getAddressDetails(voter), proposalId) - private fun getParams(param: GovParamType) = govClient.getParams(param) + private fun getParamsAtHeight(param: GovParamType, height: Int) = + runBlocking { govClient.getParamsAtHeight(param, height) } + + private fun getParams(param: GovParamType) = runBlocking { govClient.getParams(param) } - private fun getDepositPercentage(proposalId: Long) = transaction { + private fun getDepositPercentage(proposalId: Long, depositParamHeight: Int) = transaction { val (initial, current) = GovDepositRecord.findByProposalId(proposalId).filter { it.denom == NHASH } .let { list -> val current = list.sumOf { it.amount } @@ -185,10 +218,61 @@ class GovService( list.firstOrNull { it.depositType == DepositType.INITIAL_DEPOSIT.name }?.amount ?: BigDecimal.ZERO initial to current } - val needed = getParams(GovParamType.deposit).depositParams.minDepositList.first { it.denom == NHASH }.amount + val needed = ( + getParamsAtHeight(GovParamType.deposit, depositParamHeight) + ?: getParams(GovParamType.deposit) + ).depositParams.minDepositList.first { it.denom == NHASH }.amount DepositPercentage(initial.stringfy(), current.stringfy(), needed, NHASH) } + private fun getVotingDetails(proposalId: Long) = transaction { + val proposal = GovProposalRecord.findByProposalId(proposalId) + ?: throw ResourceNotFoundException("Invalid proposal id: '$proposalId'") + + val params = + ( + getParamsAtHeight(GovParamType.tallying, proposal.votingParamCheckHeight) + ?: getParams(GovParamType.tallying) + ) + .tallyParams.let { param -> + val eligibleAmount = + ( + if (proposal.votingParamCheckHeight == -1) cacheService.getSpotlight()!!.latestBlock.height + else proposal.votingParamCheckHeight + ) + .let { valService.getValidatorsByHeight(it + 1) } + .validatorsList + .sumOf { it.votingPower } + // Voting power is in mhash, not hash. So pad up to nhash for FE UI conversion + .mhashToNhash() + + TallyParams( + CoinStr(eligibleAmount.toString(), NHASH), + param.quorum.toStringUtf8().toDecimalString(), + param.threshold.toStringUtf8().toDecimalString(), + param.vetoThreshold.toStringUtf8().toDecimalString() + ) + } + val dbRecords = GovVoteRecord.findByProposalId(proposalId) + val indTallies = dbRecords.groupingBy { it.vote }.eachCount() + val tallies = runBlocking { govClient.getTally(proposalId)?.tally } + val zeroStr = 0.toString() + val tally = VotesTally( + Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_YES.name, 0), CoinStr(tallies?.yes ?: zeroStr, NHASH)), + Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_NO.name, 0), CoinStr(tallies?.no ?: zeroStr, NHASH)), + Tally( + indTallies.getOrDefault(VoteOption.VOTE_OPTION_NO_WITH_VETO.name, 0), + CoinStr(tallies?.noWithVeto ?: zeroStr, NHASH) + ), + Tally( + indTallies.getOrDefault(VoteOption.VOTE_OPTION_ABSTAIN.name, 0), + CoinStr(tallies?.abstain ?: zeroStr, NHASH) + ), + Tally(dbRecords.count(), CoinStr(tallies?.sum()?.stringfy() ?: zeroStr, NHASH)) + ) + VotingDetails(params, tally) + } + private fun getAddressObj(addr: String, isValidator: Boolean) = transaction { val (operatorAddress, moniker) = if (isValidator) @@ -209,7 +293,8 @@ class GovService( record.content ), ProposalTimings( - getDepositPercentage(record.proposalId), + getDepositPercentage(record.proposalId, record.depositParamCheckHeight), + getVotingDetails(record.proposalId), record.data.submitTime.formattedString(), record.data.depositEndTime.formattedString(), GovTimeFrame(record.data.votingStartTime.formattedString(), record.data.votingEndTime.formattedString()) @@ -231,28 +316,59 @@ class GovService( ?: throw ResourceNotFoundException("Invalid proposal id: '$proposalId'") } + fun getProposalVotesPaginated(proposalId: Long, page: Int, count: Int) = transaction { + GovVoteRecord.findByProposalIdPaginated(proposalId, count, page.toOffset(count)) + .map { mapVoteRecordFromAgg(it) } + .let { + val total = GovVoteRecord.findByProposalIdCount(proposalId) + PagedResults(total.pageCountOfResults(count), it, total) + } + } + + @Deprecated("Data split out: For vote list, use GovService.getProposalVotesPaginated; For vote tallies, use GovService.getProposalDetail") fun getProposalVotes(proposalId: Long) = transaction { - val params = getParams(GovParamType.tallying).tallyParams.let { param -> - TallyParams( - CoinStr(valService.getStakingValidators(ACTIVE).sumOf { it.tokenCount }.stringfy(), NHASH), - param.quorum.toStringUtf8().toDecCoin(), - param.threshold.toStringUtf8().toDecCoin(), - param.vetoThreshold.toStringUtf8().toDecCoin() - ) - } + val proposal = GovProposalRecord.findByProposalId(proposalId) + ?: throw ResourceNotFoundException("Invalid proposal id: '$proposalId'") + + val params = + ( + getParamsAtHeight(GovParamType.tallying, proposal.votingParamCheckHeight) + ?: getParams(GovParamType.tallying) + ) + .tallyParams.let { param -> + val eligibleAmount = + ( + if (proposal.votingParamCheckHeight == -1) cacheService.getSpotlight()!!.latestBlock.height + else proposal.votingParamCheckHeight + ) + .let { valService.getValidatorsByHeight(it) } + .validatorsList + .sumOf { it.votingPower } + + TallyParams( + CoinStr(eligibleAmount.toString(), NHASH), + param.quorum.toStringUtf8().toDecimalString(), + param.threshold.toStringUtf8().toDecimalString(), + param.vetoThreshold.toStringUtf8().toDecimalString() + ) + } val dbRecords = GovVoteRecord.findByProposalId(proposalId) val voteRecords = dbRecords.groupBy { it.voter }.map { (k, v) -> mapVoteRecord(k, v) } val indTallies = dbRecords.groupingBy { it.vote }.eachCount() - val tallies = govClient.getTally(proposalId).tally + val tallies = runBlocking { govClient.getTally(proposalId)?.tally } + val zeroStr = 0.toString() val tally = VotesTally( - Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_YES.name, 0), CoinStr(tallies.yes, NHASH)), - Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_NO.name, 0), CoinStr(tallies.no, NHASH)), + Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_YES.name, 0), CoinStr(tallies?.yes ?: zeroStr, NHASH)), + Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_NO.name, 0), CoinStr(tallies?.no ?: zeroStr, NHASH)), Tally( indTallies.getOrDefault(VoteOption.VOTE_OPTION_NO_WITH_VETO.name, 0), - CoinStr(tallies.noWithVeto, NHASH) + CoinStr(tallies?.noWithVeto ?: zeroStr, NHASH) ), - Tally(indTallies.getOrDefault(VoteOption.VOTE_OPTION_ABSTAIN.name, 0), CoinStr(tallies.abstain, NHASH)), - Tally(dbRecords.count(), CoinStr(tallies.sum().stringfy(), NHASH)) + Tally( + indTallies.getOrDefault(VoteOption.VOTE_OPTION_ABSTAIN.name, 0), + CoinStr(tallies?.abstain ?: zeroStr, NHASH) + ), + Tally(dbRecords.count(), CoinStr(tallies?.sum()?.stringfy() ?: zeroStr, NHASH)) ) GovVotesDetail(params, tally, voteRecords) } @@ -275,6 +391,18 @@ class GovService( ) } + private fun mapVoteRecordFromAgg(record: VoteDbRecordAgg) = + VoteRecord( + getAddressObj(record.voter, record.isValidator), + record.voteWeight.associate { it.vote to it.weight }.fillVoteSet(), + record.blockHeight, + record.txHash, + record.txTimestamp.toString(), + record.proposalId, + record.proposalTitle, + record.proposalStatus + ) + private fun Map.fillVoteSet() = (VoteOption.values().toList() - VoteOption.VOTE_OPTION_UNSPECIFIED - VoteOption.UNRECOGNIZED) .associate { it.name to null } + this @@ -319,6 +447,8 @@ fun Any.getGovMsgDetail(txHash: String) = } typeUrl.endsWith("MsgVote") -> this.toMsgVote().let { GovMsgDetail(null, "", it.proposalId, "") } + typeUrl.endsWith("MsgVoteWeighted") -> + this.toMsgVoteWeighted().let { GovMsgDetail(null, "", it.proposalId, "") } typeUrl.endsWith("MsgDeposit") -> this.toMsgDeposit().let { GovMsgDetail(it.amountList.first().toData(), "", it.proposalId, "") } else -> null.also { logger().debug("This typeUrl is not a governance-based msg: $typeUrl") } diff --git a/service/src/main/kotlin/io/provenance/explorer/service/NotificationService.kt b/service/src/main/kotlin/io/provenance/explorer/service/NotificationService.kt index 9d31c446..20623ced 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/NotificationService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/NotificationService.kt @@ -16,6 +16,7 @@ import io.provenance.explorer.domain.models.explorer.OpenProposals import io.provenance.explorer.domain.models.explorer.PagedResults import io.provenance.explorer.domain.models.explorer.ScheduledUpgrade import io.provenance.explorer.grpc.v1.GovGrpcClient +import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.transactions.transaction import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -43,7 +44,7 @@ class NotificationService( OpenProposals(nonUpgrades.map { it.second }, upgrades.map { it.second }) } - fun fetchScheduledUpgrades() = transaction { + fun fetchScheduledUpgrades() = runBlocking { val upgrades = GovProposalRecord.findByProposalType(govService.getUpgradeProtoType()) .filter { it.status == Gov.ProposalStatus.PROPOSAL_STATUS_PASSED.name } .filter { proposal -> diff --git a/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt b/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt index fc77ca26..f6a1c50d 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt @@ -31,8 +31,8 @@ import io.provenance.explorer.domain.extensions.pageCountOfResults import io.provenance.explorer.domain.extensions.stringfy import io.provenance.explorer.domain.extensions.toCoinStr import io.provenance.explorer.domain.extensions.toDateTime -import io.provenance.explorer.domain.extensions.toDecCoin import io.provenance.explorer.domain.extensions.toDecimal +import io.provenance.explorer.domain.extensions.toDecimalString import io.provenance.explorer.domain.extensions.toOffset import io.provenance.explorer.domain.extensions.toPercentage import io.provenance.explorer.domain.extensions.toSingleSigKeyValue @@ -243,7 +243,7 @@ class ValidatorService( ValidatorSummaryAbbrev( currVal.json.description.moniker, currVal.operatorAddress, - currVal.json.commission.commissionRates.rate.toDecCoin(), + currVal.json.commission.commissionRates.rate.toDecimalString(), getImgUrl(currVal.json.description.identity) ) } @@ -307,7 +307,7 @@ class ValidatorService( validator.votingPower.toBigInteger(), totalVotingPower ) else null, - commission = stakingVal.json.commission.commissionRates.rate.toDecCoin(), + commission = stakingVal.json.commission.commissionRates.rate.toDecimalString(), bondedTokens = CountStrTotal(stakingVal.json.tokens, null, NHASH), delegators = delegatorCount, status = stakingVal.json.getStatusString(), @@ -358,7 +358,7 @@ class ValidatorService( null, CoinStr(it.balance.amount, it.balance.denom), null, - it.delegation.shares.toDecCoin(), + it.delegation.shares.toDecimalString(), null, null ) @@ -405,12 +405,12 @@ class ValidatorService( NHASH ), delegatorCount, - validator.delegatorShares.toDecCoin(), - rewards?.amount?.toDecCoin()?.let { CoinStr(it, rewards.denom) } ?: CoinStr("0", NHASH), + validator.delegatorShares.toDecimalString(), + rewards?.amount?.toDecimalString()?.let { CoinStr(it, rewards.denom) } ?: CoinStr("0", NHASH), CommissionRate( - validator.commission.commissionRates.rate.toDecCoin(), - validator.commission.commissionRates.maxRate.toDecCoin(), - validator.commission.commissionRates.maxChangeRate.toDecCoin() + validator.commission.commissionRates.rate.toDecimalString(), + validator.commission.commissionRates.maxRate.toDecimalString(), + validator.commission.commissionRates.maxChangeRate.toDecimalString() ) ) } diff --git a/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt b/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt index e2e92166..0ff2c82b 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt @@ -522,6 +522,7 @@ class AsyncCachingV2( } val txSuccess = tx.txResponse.code == 0 + val successfulRecvHashes = mutableListOf() // save ledgers, acks tx.tx.body.messagesList.map { it.getIbcLedgerMsgs() } .forEachIndexed { idx, any -> @@ -571,6 +572,15 @@ class AsyncCachingV2( txUpdate.apply { this.ibcLedgers.add(ibcService.buildIbcLedger(ledger, txInfo, null)) } + } else if (successfulRecvHashes.contains(IbcLedgerRecord.getUniqueHash(ledger))) { + BlockTxRetryRecord.insertNonBlockingRetry( + txInfo.blockHeight, + InvalidArgumentException( + "Matching IBC Ledger record has not been saved yet - " + + "${ledger.channel!!.srcPort}/${ledger.channel!!.srcChannel}, " + + "sequence ${ledger.sequence}. Retrying block to save non-effected RECV record." + ) + ) } else { throw InvalidArgumentException( "No matching IBC ledger record for channel " + diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v1/ExplorerController.kt b/service/src/main/kotlin/io/provenance/explorer/web/v1/ExplorerController.kt deleted file mode 100644 index 0afb9917..00000000 --- a/service/src/main/kotlin/io/provenance/explorer/web/v1/ExplorerController.kt +++ /dev/null @@ -1,118 +0,0 @@ -package io.provenance.explorer.web.v1 - -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import org.joda.time.DateTime -import org.springframework.format.annotation.DateTimeFormat -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController -import javax.validation.constraints.Min - -@Deprecated("Deprecated in v=favor of V2 of the API.") -@Validated -@RestController -@RequestMapping(value = ["/api/v1"]) -@Api( - value = "Explorer controller", - produces = "application/json", - consumes = "application/json", - tags = ["General V1"], - hidden = true -) -class ExplorerController { - - @ApiOperation(value = "Return the latest block transactions") - @GetMapping(value = ["/recent/txs"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun txsRecent( - @RequestParam(defaultValue = "10") @Min(1) count: Int, - @RequestParam(defaultValue = "1") @Min(1) page: Int, - @RequestParam(defaultValue = "desc") sort: String = "desc" - ): - ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/txs/recent") - - @ApiOperation(value = "Return transaction by hash value") - @GetMapping(value = ["/tx"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun txByHash(@RequestParam(required = true) hash: String): - ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/txs/{hash}") - - @ApiOperation(value = "Return transaction by block height") - @GetMapping(value = ["/txs"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun txByBlockHeight(@RequestParam(required = true) height: Int): - ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/txs/height/{height}") - - @ApiOperation(value = "Get X-Day Transaction History") - @GetMapping(value = ["/txs/history"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun txHistory( - @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) fromDate: DateTime, - @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) toDate: DateTime, - @RequestParam(defaultValue = "day") granularity: String - ): ResponseEntity = - ResponseEntity.status(301).body("Deprecated for /api/v2/txs/history") - - @ApiOperation(value = "Return block at specified height") - @GetMapping(value = ["/block"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun blockHeight(@RequestParam(required = false) height: Int?): - ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/blocks/height/{height}") - - @ApiOperation(value = "Returns most recent blocks") - @GetMapping(value = ["/recent/blocks"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun recentBlocks( - @RequestParam(required = true, defaultValue = "10") @Min(1) count: Int, - @RequestParam(required = true, defaultValue = "1") @Min(1) page: Int, - @RequestParam(defaultValue = "desc") sort: String - ): - ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/blocks/recent") - - @ApiOperation(value = "Returns recent validators") - @GetMapping(value = ["/recent/validators"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun validatorsV2( - @RequestParam(defaultValue = "10") @Min(1) count: Int, - @RequestParam(defaultValue = "1") @Min(1) page: Int, - @RequestParam(defaultValue = "desc") sort: String, - @RequestParam(defaultValue = "BOND_STATUS_BONDED") status: String - ): - ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/validators/recent") - - @ApiOperation(value = "Returns validator by address id") - @GetMapping(value = ["/validator"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun validator(@RequestParam(required = true) id: String): ResponseEntity = - ResponseEntity.status(301).body("Deprecated for /api/v2/validators/{id}") - - @ApiOperation(value = "Returns set of validators at block height") - @GetMapping(value = ["/validators"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun validatorsAtHeight( - @RequestParam(required = true) blockHeight: Int, - @RequestParam(required = false, defaultValue = "10") @Min(1) count: Int, - @RequestParam(required = false, defaultValue = "1") @Min(1) page: Int, - @RequestParam(required = false, defaultValue = "desc") sort: String - ): - ResponseEntity = - ResponseEntity.status(301).body("Deprecated for /api/v2/validators/height/{blockHeight}") - - @ApiOperation(value = "Returns spotlight statistics") - @GetMapping(value = ["/spotlight"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun spotlight(): ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/spotlight") - - @ApiOperation(value = "Returns spotlight statistics") - @GetMapping(value = ["/gasStats"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun gasStatistics( - @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) fromDate: DateTime, - @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) toDate: DateTime, - @RequestParam(required = false, defaultValue = "day") granularity: String - ): ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/gas/statistics") - - @ApiOperation(value = "Returns transaction json") - @GetMapping(value = ["/tx/{txnHash}/json"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun transactionJson(@PathVariable txnHash: String): ResponseEntity = - ResponseEntity.status(301).body("Deprecated for /api/v2/txs/{hash}/json") - - @ApiOperation(value = "Returns the ID of the chain associated with the explorer instance") - @GetMapping(value = ["/chain/id"], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun getChainId(): ResponseEntity = ResponseEntity.status(301).body("Deprecated for /api/v2/chain/id") -} diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v2/GeneralController.kt b/service/src/main/kotlin/io/provenance/explorer/web/v2/GeneralController.kt index 636c05ca..89a2f6db 100644 --- a/service/src/main/kotlin/io/provenance/explorer/web/v2/GeneralController.kt +++ b/service/src/main/kotlin/io/provenance/explorer/web/v2/GeneralController.kt @@ -57,7 +57,7 @@ class GeneralController( value = "DateTime format as `yyyy-MM-dd` — for example, \"2000-10-31\"", required = true ) @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) toDate: DateTime, - @ApiParam(value = "The granularity of data, either DAY or HOUR", defaultValue = "DAY", required = false) + @ApiParam(value = "The granularity of data, either DAY or HOUR", defaultValue = "DAY", required = false, allowableValues = "DAY,HOUR") @RequestParam(required = false) granularity: DateTruncGranularity?, @ApiParam(value = "The message type string, ie write_scope, send, add_attribute", required = false) @RequestParam(required = false) msgType: String? @@ -76,7 +76,7 @@ class GeneralController( value = "DateTime format as `yyyy-MM-dd` — for example, \"2000-10-31\"", required = true ) @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) toDate: DateTime, - @ApiParam(value = "The granularity of data, either DAY or HOUR", defaultValue = "DAY", required = false) + @ApiParam(value = "The granularity of data, either DAY or HOUR", defaultValue = "DAY", required = false, allowableValues = "DAY,HOUR") @RequestParam(required = false) granularity: DateTruncGranularity? ) = ResponseEntity.ok(explorerService.getGasVolume(fromDate, toDate, granularity)) diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v2/GovController.kt b/service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt similarity index 95% rename from service/src/main/kotlin/io/provenance/explorer/web/v2/GovController.kt rename to service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt index 51b83242..c0ce7df9 100644 --- a/service/src/main/kotlin/io/provenance/explorer/web/v2/GovController.kt +++ b/service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt @@ -19,12 +19,12 @@ import javax.validation.constraints.Min @RestController @RequestMapping(path = ["/api/v2/gov"], produces = [MediaType.APPLICATION_JSON_VALUE]) @Api( - description = "Governance-related endpoints", + description = "Governance-related endpoints - V2", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, tags = ["Governance"] ) -class GovController(private val govService: GovService) { +class GovControllerV2(private val govService: GovService) { @ApiOperation("Returns paginated list of proposals, proposal ID descending") @GetMapping("/proposals/all") @@ -42,6 +42,7 @@ class GovController(private val govService: GovService) { @ApiOperation("Returns vote tallies and vote records of a proposal") @GetMapping("/proposals/{id}/votes") + @Deprecated("Use /api/v3/gov/proposals/{id}/votes") fun getProposalVotes( @ApiParam(value = "The ID of the proposal") @PathVariable id: Long ) = ResponseEntity.ok(govService.getProposalVotes(id)) diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v2/TransactionController.kt b/service/src/main/kotlin/io/provenance/explorer/web/v2/TransactionController.kt index f7e2e706..0c856c8d 100644 --- a/service/src/main/kotlin/io/provenance/explorer/web/v2/TransactionController.kt +++ b/service/src/main/kotlin/io/provenance/explorer/web/v2/TransactionController.kt @@ -102,7 +102,7 @@ class TransactionController(private val transactionService: TransactionService) value = "DateTime format as `yyyy-MM-dd` — for example, \"2000-10-31\"", required = true ) @RequestParam(required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) toDate: DateTime, - @ApiParam(value = "The granularity of data, either DAY or HOUR", defaultValue = "DAY", required = false) + @ApiParam(value = "The granularity of data, either DAY or HOUR", defaultValue = "DAY", required = false, allowableValues = "DAY,HOUR") @RequestParam(defaultValue = "DAY") granularity: DateTruncGranularity ) = ResponseEntity.ok(transactionService.getTxHistoryByQuery(fromDate, toDate, granularity)) diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt b/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt new file mode 100644 index 00000000..26b1ddee --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt @@ -0,0 +1,37 @@ +package io.provenance.explorer.web.v3 + +import io.provenance.explorer.service.GovService +import io.swagger.annotations.Api +import io.swagger.annotations.ApiOperation +import io.swagger.annotations.ApiParam +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import javax.validation.constraints.Max +import javax.validation.constraints.Min + +@Validated +@RestController +@RequestMapping(path = ["/api/v3/gov"], produces = [MediaType.APPLICATION_JSON_VALUE]) +@Api( + description = "Governance-related endpoints - V3", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE, + tags = ["Governance"] +) +class GovControllerV3(private val govService: GovService) { + + @ApiOperation("Returns vote tallies and vote records of a proposal") + @GetMapping("/proposals/{id}/votes") + fun getProposalVotes( + @ApiParam(value = "The ID of the proposal") @PathVariable id: Long, + @ApiParam(defaultValue = "1", required = false) @RequestParam(defaultValue = "1") @Min(1) page: Int, + @ApiParam(value = "Record count between 1 and 50", defaultValue = "10", required = false) + @RequestParam(defaultValue = "10") @Min(1) @Max(50) count: Int + ) = ResponseEntity.ok(govService.getProposalVotesPaginated(id, page, count)) +}