Skip to content

Commit

Permalink
Wrapping current gas estimators with fetchable gas pricing (#25)
Browse files Browse the repository at this point in the history
* Break out logic for gas estimations into lambda parameters.
* Associate legacy naming for parameters to newer lambdas.
* Add floating gas price fetching
* Remove `fromSimulation()`, logic moved into cosmosSimulation estimator.
  • Loading branch information
mtps committed Mar 1, 2022
1 parent 7f717ef commit 7217d98
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 42 deletions.
2 changes: 0 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ object Versions {
val ProvenanceProtos = "1.8.0-rc7"
val ProvenanceHDWallet = "0.1.15"
val BouncyCastle = "1.70"
val Kethereum = "0.83.4"
val Komputing = "0.1"
val Grpc = "1.44.0"
val Kotlin = "1.6.10"
}
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlin.code.style=official
6 changes: 6 additions & 0 deletions src/main/kotlin/io/provenance/client/TestnetFeaturePreview.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.provenance.client

@RequiresOptIn(message = "This API is experimental and only exists in test.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class TestnetFeaturePreview
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.provenance.client.gas.estimators

import cosmos.tx.v1beta1.ServiceOuterClass
import io.provenance.client.gas.prices.GasPrices
import io.provenance.client.grpc.GasEstimate
import io.provenance.client.grpc.PbGasEstimator
import io.provenance.client.internal.extensions.toCoin
import kotlin.math.ceil

/**
* Cosmos simulation gas estimation. Must be used when interacting with pbc 1.7 or lower.
* TODO - Remove once mainnet.version > 1.8
*/
internal fun cosmosSimulationGasEstimator(gasPrices: GasPrices): PbGasEstimator = {
{ tx, adjustment ->
val price = gasPrices()
val sim = cosmosService.simulate(
ServiceOuterClass.SimulateRequest.newBuilder()
.setTxBytes(tx.toByteString())
.build()
)
val limit = ceil(sim.gasInfo.gasUsed * adjustment).toLong()
val feeAmount = ceil(limit * price.amount.toDouble()).toLong()
GasEstimate(limit, listOf(feeAmount.toCoin(price.denom)))
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/io/provenance/client/gas/estimators/Floating.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.provenance.client.gas.estimators

// import io.provenance.client.TestnetFeaturePreview
import io.provenance.client.gas.prices.GasPrices
import io.provenance.client.grpc.PbGasEstimator
import io.provenance.client.internal.extensions.times

// @TestnetFeaturePreview
internal fun floatingGasPriceGasEstimator(delegate: PbGasEstimator, floatingGasPrice: GasPrices): PbGasEstimator = {
{ tx, adjustment ->
val price = floatingGasPrice()
require(price.denom == "nhash") { "only nhash is supported for fees" }

// Original estimate
val estimate = delegate(this)(tx, adjustment)
// Adjust up or down based on floating factor.
val factor = price.amount.toDouble() / nodeGasPrice.value
// Updated values
estimate.copy(feesCalculated = estimate.feesCalculated * factor)
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/io/provenance/client/gas/estimators/MsgFees.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.provenance.client.gas.estimators

import io.provenance.client.grpc.GasEstimate
import io.provenance.client.grpc.PbGasEstimator
import io.provenance.msgfees.v1.CalculateTxFeesRequest

/**
* Message fee endpoint gas estimation. Only compatible and should be used with pbc 1.8 or greater.
*/
// @TestnetFeaturePreview
internal val MsgFeeCalculationGasEstimator: PbGasEstimator = {
{ tx, adjustment ->
val estimate = msgFeeClient.calculateTxFees(
CalculateTxFeesRequest.newBuilder()
.setTxBytes(tx.toByteString())
.setGasAdjustment(adjustment.toFloat())
.build()
)
GasEstimate(estimate.estimatedGas, estimate.totalFeesList)
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/io/provenance/client/gas/prices/CachedGasPrice.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.provenance.client.gas.prices

import cosmos.base.v1beta1.CoinOuterClass
import io.provenance.client.internal.extensions.toCoin
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration

/**
* Cache the gas prices for a determined period of time
*/
class CachedGasPrice(private val gasPrices: GasPrices, private val duration: Duration) : GasPrices {
private val lastFetch = AtomicReference(OffsetDateTime.MIN)
private val cachedValue = AtomicReference("0nhash".toCoin())

override fun invoke(): CoinOuterClass.Coin {
if (OffsetDateTime.now().isAfter(lastFetch.get().plus(duration.inWholeMilliseconds, ChronoUnit.MILLIS))) {
cachedValue.set(gasPrices())
lastFetch.set(OffsetDateTime.now())
}
return cachedValue.get()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.provenance.client.gas.prices

import cosmos.base.v1beta1.CoinOuterClass

class ConstantGasPrice(private val gasPrice: CoinOuterClass.Coin) : GasPrices {
override fun invoke(): CoinOuterClass.Coin = gasPrice
}
9 changes: 9 additions & 0 deletions src/main/kotlin/io/provenance/client/gas/prices/GasPrices.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.provenance.client.gas.prices

import cosmos.base.v1beta1.CoinOuterClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours

typealias GasPrices = () -> CoinOuterClass.Coin

fun GasPrices.cached(ttl: Duration = 1.hours): GasPrices = CachedGasPrice(this, ttl)
35 changes: 35 additions & 0 deletions src/main/kotlin/io/provenance/client/gas/prices/UrlGasPrices.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.provenance.client.gas.prices

import com.google.gson.Gson
import cosmos.base.v1beta1.CoinOuterClass
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.HttpClientBuilder
import java.io.InputStreamReader

/**
* When provided with a url, fetches an object of shape '{"gasPrice":nnn,"gasPriceDenom":"denom"}'
*/
open class UrlGasPrices(private val uri: String) : GasPrices {
private val client = HttpClientBuilder.create().build()
private val gson = Gson().newBuilder().create()

private data class GasPrice(val gasPrice: Int, val gasPriceDenom: String) {
fun toCoin(): CoinOuterClass.Coin = CoinOuterClass.Coin.newBuilder()
.setAmount(gasPrice.toString())
.setDenom(gasPriceDenom)
.build()
}

override fun invoke(): CoinOuterClass.Coin {
val result = client.execute(HttpGet(uri))
require(result.statusLine.statusCode in 200..299) {
"failed to get uri:$uri status:${result.statusLine.statusCode}: ${result.statusLine.reasonPhrase}"
}

return result.entity.content.use { i ->
InputStreamReader(i).use {
gson.fromJson(it, GasPrice::class.java).toCoin()
}
}
}
}
72 changes: 52 additions & 20 deletions src/main/kotlin/io/provenance/client/grpc/GasEstimate.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package io.provenance.client.grpc

import cosmos.base.v1beta1.CoinOuterClass
import kotlin.math.ceil
import cosmos.tx.v1beta1.TxOuterClass
// import io.provenance.client.TestnetFeaturePreview
import io.provenance.client.gas.estimators.MsgFeeCalculationGasEstimator
import io.provenance.client.gas.estimators.cosmosSimulationGasEstimator
import io.provenance.client.gas.estimators.floatingGasPriceGasEstimator
import io.provenance.client.gas.prices.GasPrices
import io.provenance.client.internal.extensions.toCoin

/**
* The gas estimate implementation
Expand All @@ -10,44 +16,70 @@ import kotlin.math.ceil
* @param feesCalculated A list of [CoinOuterClass.Coin].
*/
data class GasEstimate(val limit: Long, val feesCalculated: List<CoinOuterClass.Coin> = emptyList()) {

companion object {
const val DEFAULT_FEE_ADJUSTMENT = 1.25
const val DEFAULT_GAS_PRICE = 1905.00

// TODO - Remove once mainnet.version > 1.8
@Deprecated("do not use")
internal const val DEFAULT_GAS_PRICE = 1905.00
}
}

/**
* Creates a [GasEstimate] when [GasEstimationMethod.COSMOS_SIMULATION] is used.
*
* @param estimate The estimated gas limit.
* @param adjustment An adjustment to apply to the gas estimate.
* [GasEstimator] is an alias to standardize how gas estimations are made.
* @param tx The transaction to estimate for.
* @param adj The gas adjustment being applied.
* @return Gas estimates.
*/
fun fromSimulation(estimate: Long, adjustment: Double): GasEstimate {
val limit = ceil(estimate * adjustment).toLong()
val feeAmount = ceil(limit * GasEstimate.DEFAULT_GAS_PRICE).toLong()
return listOf(
CoinOuterClass.Coin.newBuilder()
.setAmount(feeAmount.toString())
.setDenom("nhash")
.build()
).let { fees -> GasEstimate(limit, fees) }
}
typealias GasEstimator = (tx: TxOuterClass.Tx, adjustment: Double) -> GasEstimate

/**
* Wrapper alias for estimation methods to allow scoping of pbClient GRPC methods into the estimation.
*/
typealias PbGasEstimator = PbClient.() -> GasEstimator

/**
* A set of flags used to specify how gas should be estimated
*
* - `COSMOS_SIMULATION` - A flag to specify cosmos simulation gas estimation. Must be used when interacting with pbc 1.7 or lower.
* - `MSG_FEE_CALCULATION` - A flag to specify message fee endpoint gas estimation. Only compatible and should be used with pbc 1.8 or greater.
*/
enum class GasEstimationMethod {
object GasEstimationMethod {
/**
* A flag for cosmos simulation gas estimation. Must be used when interacting with pbc 1.7 or lower.
*/
COSMOS_SIMULATION,
val COSMOS_SIMULATION: PbGasEstimator = cosmosSimulationGasEstimator { GasEstimate.DEFAULT_GAS_PRICE.toCoin("nhash") }

/**
* A flag for message fee endpoint gas estimation. Only compatible and should be used with pbc 1.8 or greater.
*/
MSG_FEE_CALCULATION
// @TestnetFeaturePreview
val MSG_FEE_CALCULATION: PbGasEstimator = MsgFeeCalculationGasEstimator
}

/**
* Add a floating adjustment based on current market rates to estimations.
*
* Example:
*
* ```kotlin
* val priceGetter = UrlGasPrices("https://oracle.my.domain/gas-price").cached()
*
* val estimator =
* if (provenance.network.version < 1.8) GasEstimationMethod.COSMOS_SIMULATION
* else GasEstimationMethod.MSG_FEE_CALCULATION
*
* val pbClient = PbClient(
* chainId = "pio-testnet-1",
* channelUri = URI("grpcs://grpc.test.provenance.io"),
* gasEstimationMethod = floatingGasPrices(estimator, priceGetter)
* )
* ```
*
* @param delegate The underlying estimation calculation methods to use.
* @param gasPrices The current gas price supplier.
* @return [PbGasEstimator]
*/
// @TestnetFeaturePreview
fun floatingGasPrices(delegate: PbGasEstimator, gasPrices: GasPrices): PbGasEstimator =
floatingGasPriceGasEstimator(delegate, gasPrices)
33 changes: 13 additions & 20 deletions src/main/kotlin/io/provenance/client/grpc/PbClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cosmos.tx.v1beta1.TxOuterClass
import cosmos.tx.v1beta1.TxOuterClass.TxBody
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder
import io.provenance.client.protobuf.extensions.getBaseAccount
import io.provenance.msgfees.v1.CalculateTxFeesRequest
import io.provenance.msgfees.v1.QueryParamsRequest
import java.io.Closeable
import java.net.URI
import java.util.concurrent.ExecutorService
Expand All @@ -24,7 +24,7 @@ data class ChannelOpts(
open class PbClient(
val chainId: String,
val channelUri: URI,
val gasEstimationMethod: GasEstimationMethod,
val gasEstimationMethod: PbGasEstimator,
opts: ChannelOpts = ChannelOpts(),
channelConfigLambda: (NettyChannelBuilder) -> Unit = { }
) : Closeable {
Expand Down Expand Up @@ -68,7 +68,10 @@ open class PbClient(
val markerClient = io.provenance.marker.v1.QueryGrpc.newBlockingStub(channel)
val metadataClient = io.provenance.metadata.v1.QueryGrpc.newBlockingStub(channel)
val mintClient = cosmos.mint.v1beta1.QueryGrpc.newBlockingStub(channel)

// @TestnetFeaturePreview
val msgFeeClient = io.provenance.msgfees.v1.QueryGrpc.newBlockingStub(channel)

val nameClient = io.provenance.name.v1.QueryGrpc.newBlockingStub(channel)
val paramsClient = cosmos.params.v1beta1.QueryGrpc.newBlockingStub(channel)
val slashingClient = cosmos.slashing.v1beta1.QueryGrpc.newBlockingStub(channel)
Expand All @@ -77,6 +80,12 @@ open class PbClient(
val upgradeClient = cosmos.upgrade.v1beta1.QueryGrpc.newBlockingStub(channel)
val wasmClient = cosmwasm.wasm.v1.QueryGrpc.newBlockingStub(channel)

// @TestnetFeaturePreview
val nodeFeeParams = lazy { msgFeeClient.params(QueryParamsRequest.getDefaultInstance()).params }

// @TestnetFeaturePreview
val nodeGasPrice = lazy { nodeFeeParams.value.floorGasPrice.amount.toDouble() }

fun baseRequest(
txBody: TxBody,
signers: List<BaseReqSigner>,
Expand Down Expand Up @@ -111,24 +120,8 @@ open class PbClient(
}.let { signatures ->
val signedTx = tx.toBuilder().addAllSignatures(signatures).build()
val gasAdjustment = baseReq.gasAdjustment ?: GasEstimate.DEFAULT_FEE_ADJUSTMENT

when (gasEstimationMethod) {
GasEstimationMethod.COSMOS_SIMULATION -> {
cosmosService.simulate(
ServiceOuterClass.SimulateRequest.newBuilder()
.setTxBytes(signedTx.toByteString())
.build()
).let { sim -> fromSimulation(sim.gasInfo.gasUsed, gasAdjustment) }
}
GasEstimationMethod.MSG_FEE_CALCULATION -> {
msgFeeClient.calculateTxFees(
CalculateTxFeesRequest.newBuilder()
.setTxBytes(signedTx.toByteString())
.setGasAdjustment(gasAdjustment.toFloat())
.build()
).let { msgFee -> GasEstimate(msgFee.estimatedGas, msgFee.totalFeesList) }
}
}
val gasEstimator = gasEstimationMethod()
gasEstimator(signedTx, gasAdjustment)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.provenance.client.internal.extensions

import cosmos.base.v1beta1.CoinOuterClass

internal fun Number.toCoin(denom: String): CoinOuterClass.Coin {
return CoinOuterClass.Coin.newBuilder()
.setAmount(toString())
.setDenom(denom)
.build()
}

internal fun String.toCoin(): CoinOuterClass.Coin {
val split = indexOfFirst { it.isLetter() }
require(split != 0) { "invalid amount for coin:$this" }
require(split > 0) { "invalid denom for coin:$this" }

return CoinOuterClass.Coin.newBuilder()
.setAmount(substring(0, split))
.setDenom(substring(split, length))
.build()
}

internal operator fun List<CoinOuterClass.Coin>.times(other: Double): List<CoinOuterClass.Coin> = map { it * other }

internal operator fun CoinOuterClass.Coin.times(other: Double): CoinOuterClass.Coin {
return CoinOuterClass.Coin
.newBuilder()
.mergeFrom(this)
.setAmount((amount.toDouble() * other).toBigDecimal().toPlainString())
.build()
}
2 changes: 2 additions & 0 deletions src/test/kotlin/io/provenance/client/PbClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import org.junit.Ignore
import java.net.URI
import kotlin.test.Test
import kotlin.test.assertTrue

@Ignore
// @OptIn(TestnetFeaturePreview::class)
class PbClientTest {

val pbClient = PbClient(
Expand Down

0 comments on commit 7217d98

Please sign in to comment.