solution: initial code for fee estimation

This commit is contained in:
Igor Artamonov
2021-12-06 23:15:29 -05:00
parent 8f423d99d2
commit 8de75003a8
22 changed files with 1323 additions and 18 deletions

View File

@@ -43,7 +43,8 @@ class BlockchainRpc(
@Autowired private val trackTx: List<TrackTx>,
@Autowired private val trackAddress: List<TrackAddress>,
@Autowired private val describe: Describe,
@Autowired private val subscribeStatus: SubscribeStatus
@Autowired private val subscribeStatus: SubscribeStatus,
@Autowired private val estimateFee: EstimateFee
) : ReactorBlockchainGrpc.BlockchainImplBase() {
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
@@ -165,6 +166,10 @@ class BlockchainRpc(
}
}
override fun estimateFee(request: Mono<BlockchainOuterClass.EstimateFeeRequest>): Mono<BlockchainOuterClass.EstimateFeeResponse> {
return request.flatMap { estimateFee.estimateFee(it) }
}
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
describeMetric.increment()
return describe.describe(request)

View File

@@ -0,0 +1,38 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.grpc.Chain
import io.grpc.Status
import io.grpc.StatusException
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@Service
class EstimateFee(
@Autowired private val multistreamHolder: MultistreamHolder
) {
companion object {
private val log = LoggerFactory.getLogger(EstimateFee::class.java)
}
fun estimateFee(req: BlockchainOuterClass.EstimateFeeRequest): Mono<BlockchainOuterClass.EstimateFeeResponse> {
val chain = Chain.byId(req.chainValue)
val up = multistreamHolder.getUpstream(chain) ?: return Mono.error(
StatusException(
Status.UNAVAILABLE.withDescription("BLOCKCHAIN UNAVAILABLE: ${req.chainValue}")
)
)
val mode = ChainFees.extractMode(req) ?: return Mono.error(
StatusException(
Status.UNAVAILABLE.withDescription("UNSUPPORTED MODE: ${req.mode.number}")
)
)
return up.getFeeEstimation()
.estimate(mode, req.blocks)
}
}

View File

@@ -0,0 +1,131 @@
package io.emeraldpay.dshackle.upstream
import com.github.benmanes.caffeine.cache.Caffeine
import io.emeraldpay.api.proto.BlockchainOuterClass
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
import java.util.EnumMap
import java.util.function.Function
abstract class AbstractChainFees<F, B, TR, T>(
private val heightLimit: Int,
private val upstreams: Multistream,
extractTx: (B) -> List<TR>?
) : ChainFees {
companion object {
private val log = LoggerFactory.getLogger(AbstractChainFees::class.java)
}
private val txSource = EnumMap<ChainFees.Mode, TxAt<B, TR>>(ChainFees.Mode::class.java)
init {
txSource[ChainFees.Mode.AVG_TOP] = TxAtTop(extractTx)
txSource[ChainFees.Mode.MIN_ALWAYS] = TxAtBottom(extractTx)
txSource[ChainFees.Mode.AVG_MIDDLE] = TxAtMiddle(extractTx)
txSource[ChainFees.Mode.AVG_LAST] = TxAtBottom(extractTx)
txSource[ChainFees.Mode.AVG_T5] = TxAtPos(extractTx, 5)
txSource[ChainFees.Mode.AVG_T20] = TxAtPos(extractTx, 20)
txSource[ChainFees.Mode.AVG_T50] = TxAtPos(extractTx, 50)
}
override fun estimate(mode: ChainFees.Mode, blocks: Int): Mono<BlockchainOuterClass.EstimateFeeResponse> {
return usingBlocks(blocks)
.flatMap { readFeesAt(it, mode) }
.transform(feeAggregation(mode))
.next()
.map(getResponseBuilder())
}
// ---
private val feeCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(60))
.build<Pair<Long, ChainFees.Mode>, F>()
fun usingBlocks(exp: Int): Flux<Long> {
val useBlocks = exp.coerceAtMost(heightLimit).coerceAtLeast(1)
val height = upstreams.getHead().getCurrentHeight()
?: return Mono.fromCallable { log.warn("Upstream is not ready. No current height") }.thenMany(Mono.empty()) // TODO or throw an exception to build a gRPC error?
val startBlock: Int = height.toInt() - useBlocks + 1
if (startBlock < 0) {
log.warn("Blockchain doesn't have enough blocks. Height: $height")
return Flux.empty()
}
return Flux.range(startBlock, useBlocks).map { it.toLong() }
}
fun readFeesAt(height: Long, mode: ChainFees.Mode): Mono<F> {
val current = feeCache.getIfPresent(Pair(height, mode))
if (current != null) {
return Mono.just(current)
}
val txSelector = txSourceFor(mode)
return readFeesAt(height, txSelector).doOnNext {
// TODO it may be EMPTY for some blocks (ex. a no tx block), so nothing gets cached and goes to do the same call each time. so do cache empty values to avoid useless requests
feeCache.put(Pair(height, mode), it!!)
}
}
open fun txSourceFor(mode: ChainFees.Mode): TxAt<B, TR> {
return txSource[mode] ?: throw IllegalStateException("No TS Source for mode $mode")
}
abstract fun readFeesAt(height: Long, selector: TxAt<B, TR>): Mono<F>
abstract fun feeAggregation(mode: ChainFees.Mode): Function<Flux<F>, Mono<F>>
abstract fun getResponseBuilder(): Function<F, BlockchainOuterClass.EstimateFeeResponse>
abstract class TxAt<B, TR>(private val extractTx: Function<B, List<TR>?>) {
fun get(block: B): TR? {
val txes = extractTx.apply(block) ?: return null
return get(txes)
}
abstract fun get(transactions: List<TR>): TR?
}
class TxAtPos<B, TR>(extractTx: Function<B, List<TR>?>, private val pos: Int) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
val index = pos.coerceAtMost(transactions.size - 1)
if (index < 0) {
return null
}
return transactions[transactions.size - index - 1]
}
}
class TxAtTop<B, TR>(extractTx: Function<B, List<TR>?>) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
if (transactions.isEmpty()) {
return null
}
return transactions[0]
}
}
class TxAtBottom<B, TR>(extractTx: Function<B, List<TR>?>) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
if (transactions.isEmpty()) {
return null
}
return transactions.last()
}
}
class TxAtMiddle<B, TR>(extractTx: Function<B, List<TR>?>) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
if (transactions.isEmpty()) {
return null
}
if (transactions.size == 1) {
return transactions[0]
}
return transactions[transactions.size / 2]
}
}
}

View File

@@ -0,0 +1,35 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.api.proto.BlockchainOuterClass
import reactor.core.publisher.Mono
interface ChainFees {
companion object {
fun extractMode(req: BlockchainOuterClass.EstimateFeeRequest): Mode? {
return when (req.mode!!) {
BlockchainOuterClass.FeeEstimationMode.INVALID -> null
BlockchainOuterClass.FeeEstimationMode.AVG_LAST -> Mode.AVG_LAST
BlockchainOuterClass.FeeEstimationMode.AVG_T5 -> Mode.AVG_T5
BlockchainOuterClass.FeeEstimationMode.AVG_T20 -> Mode.AVG_T20
BlockchainOuterClass.FeeEstimationMode.AVG_T50 -> Mode.AVG_T50
BlockchainOuterClass.FeeEstimationMode.MIN_ALWAYS -> Mode.MIN_ALWAYS
BlockchainOuterClass.FeeEstimationMode.AVG_MIDDLE -> Mode.AVG_MIDDLE
BlockchainOuterClass.FeeEstimationMode.AVG_TOP -> Mode.AVG_TOP
BlockchainOuterClass.FeeEstimationMode.UNRECOGNIZED -> null
}
}
}
fun estimate(mode: Mode, blocks: Int): Mono<BlockchainOuterClass.EstimateFeeResponse>
enum class Mode {
AVG_LAST,
AVG_T5,
AVG_T20,
AVG_T50,
MIN_ALWAYS,
AVG_MIDDLE,
AVG_TOP
}
}

View File

@@ -155,6 +155,8 @@ abstract class Multistream(
.switchIfEmpty(Mono.error(Exception("No API available for $chain")))
}
abstract fun getFeeEstimation(): ChainFees
/**
* Finds an API that leverages caches and other optimizations/transformations of the request.
*/

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.bitcoin
import java.math.BigDecimal
import java.math.BigInteger
class BitcoinConst {
companion object {
val COIN: BigInteger = BigInteger.TEN.pow(8)
val COIN_DEC: BigDecimal = COIN.toBigDecimal()
}
}

View File

@@ -0,0 +1,146 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.AbstractChainFees
import io.emeraldpay.dshackle.upstream.ChainFees
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigDecimal
import java.util.function.Function
class BitcoinFees(
upstreams: BitcoinMultistream,
private val reader: BitcoinReader,
heightLimit: Int,
) : AbstractChainFees<BitcoinFees.TxFee, Map<String, Any>, String, Map<String, Any>>(heightLimit, upstreams, extractTx), ChainFees {
companion object {
private val log = LoggerFactory.getLogger(BitcoinFees::class.java)
private val extractTx = { block: Map<String, Any> ->
block["tx"]
?.let { it as List<String> }
// drop first tx which is a miner reward
?.let { if (it.isEmpty()) it else it.drop(1) }
?: emptyList()
}
}
fun calculateFee(tx: Map<String, Any>): Mono<Long> {
val outAmount = extractVOuts(tx).reduce { acc, l -> (acc ?: 0L) + (l ?: 0L) } ?: 0
val vinsData = tx.get("vin")?.let { it as List<Map<String, Any>> } ?: return Mono.empty()
return Flux.fromIterable(vinsData)
.flatMap {
val txid = it["txid"] as String?
val vout = (it["vout"] as Number?)?.toInt()
if (txid == null || vout == null) {
Mono.empty()
} else {
getTxOutAmount(txid, vout)
}
}
.reduce { t, u -> t + u }
.map { inAmount ->
(inAmount - outAmount).coerceAtLeast(0)
}
}
fun extractSize(tx: Map<String, Any>): Int {
val size: Number = if (tx.containsKey("vsize")) {
tx["vsize"] as Number
} else if (tx.containsKey("size")) {
tx["size"] as Number
} else {
0
}
return size.toInt()
}
fun getTxOutAmount(txid: String, vout: Int): Mono<Long> {
return reader.getTx(txid)
.switchIfEmpty(
Mono.fromCallable { log.warn("No tx $txid") }
.then(Mono.empty())
)
.flatMap {
extractVOuts(it).let {
if (vout < it.size) {
Mono.justOrEmpty(it[vout])
} else {
Mono.empty()
}
}
}
}
fun extractVOuts(tx: Map<String, Any>): List<Long?> {
val voutsData = tx.get("vout")?.let { it as List<Map<String, Any>> } ?: return emptyList()
return voutsData.map {
val amount = it["value"] ?: return@map null
BigDecimal(amount.toString())
.multiply(BitcoinConst.COIN_DEC)
.longValueExact()
}
}
override fun readFeesAt(height: Long, selector: TxAt<Map<String, Any>, String>): Mono<TxFee> {
return reader.getBlock(height)
.flatMap { block ->
Mono.justOrEmpty(selector.get(block))
.flatMap { txid -> reader.getTx(txid!!) }
.flatMap { tx ->
calculateFee(tx)
.map { fee ->
TxFee(1, fee * 1024 / extractSize(tx))
}
}
}
}
override fun feeAggregation(mode: ChainFees.Mode): Function<Flux<TxFee>, Mono<TxFee>> {
if (mode == ChainFees.Mode.MIN_ALWAYS) {
return Function { src ->
src.reduce { a, b ->
if (a.fee > b.fee) a else b
}
}
}
return Function { src ->
src.reduce { a, b ->
TxFee(a.count + b.count, a.fee + b.fee)
}
}
}
override fun getResponseBuilder(): Function<TxFee, BlockchainOuterClass.EstimateFeeResponse> {
return Function {
val fee = (it.fee / it.count).coerceAtLeast(1)
BlockchainOuterClass.EstimateFeeResponse.newBuilder()
.setBitcoinStd(
BlockchainOuterClass.BitcoinStdFees.newBuilder()
.setSatPerKb(fee.toString())
)
.build()
}
}
// -------
data class TxFee(val count: Int, val fee: Long)
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.EmptyHead
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.MergedHead
@@ -49,6 +50,7 @@ open class BitcoinMultistream(
private var reader = BitcoinReader(this, head, esplora)
private var addressActiveCheck: AddressActiveCheck? = null
private var xpubAddresses: XpubAddresses? = null
private val feeEstimation = BitcoinFees(this, reader, 6)
override fun init() {
if (upstreams.size > 0) {
@@ -57,6 +59,10 @@ open class BitcoinMultistream(
super.init()
}
override fun getFeeEstimation(): ChainFees {
return feeEstimation
}
open fun getXpubAddresses(): XpubAddresses? {
return xpubAddresses
}

View File

@@ -55,6 +55,11 @@ open class BitcoinReader(
return castedRead(JsonRpcRequest("getblock", listOf(hash)), Map::class.java).cast()
}
open fun getBlock(height: Long): Mono<Map<String, Any>> {
return castedRead(JsonRpcRequest("getblockhash", listOf(height)), String::class.java)
.flatMap(this@BitcoinReader::getBlock)
}
open fun getTx(txid: String): Mono<Map<String, Any>> {
return castedRead(JsonRpcRequest("getrawtransaction", listOf(txid, true)), Map::class.java).cast()
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.AbstractChainFees
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.util.function.Tuples
import java.util.function.Function
abstract class EthereumFees(
upstreams: EthereumMultistream,
private val reader: EthereumReader,
heightLimit: Int,
) : AbstractChainFees<EthereumFees.EthereumFee, BlockJson<TransactionRefJson>, TransactionRefJson, TransactionJson>(heightLimit, upstreams, extractTx), ChainFees {
companion object {
private val log = LoggerFactory.getLogger(EthereumFees::class.java)
private val extractTx = { block: BlockJson<TransactionRefJson> ->
block.transactions
}
}
abstract fun extractFee(block: BlockJson<TransactionRefJson>, tx: TransactionJson): EthereumFee
override fun readFeesAt(height: Long, selector: TxAt<BlockJson<TransactionRefJson>, TransactionRefJson>): Mono<EthereumFee> {
return reader.blocksByHeightParsed().read(height)
.flatMap { block ->
Mono.justOrEmpty(selector.get(block))
.cast(TransactionRefJson::class.java)
.flatMap { reader.txByHash().read(it.hash) }
.map { tx -> extractFee(block, tx) }
}
}
override fun feeAggregation(mode: ChainFees.Mode): Function<Flux<EthereumFee>, Mono<EthereumFee>> {
if (mode == ChainFees.Mode.MIN_ALWAYS) {
return Function { src ->
src.reduce { a, b ->
EthereumFee(
a.max.coerceAtLeast(b.max),
a.priority.coerceAtLeast(b.priority),
a.paid.coerceAtLeast(b.paid),
Wei.ZERO
)
}
}
}
return Function { src ->
src.map { Tuples.of(1, it) }
.reduce { a, b ->
Tuples.of(a.t1 + b.t1, a.t2.plus(b.t2))
}.map {
EthereumFee(it.t2.max / it.t1, it.t2.priority / it.t1, it.t2.paid / it.t1, it.t2.base / it.t1)
}
}
}
// ---
data class EthereumFee(val max: Wei, val priority: Wei, val paid: Wei, val base: Wei) {
fun plus(o: EthereumFee): EthereumFee {
return EthereumFee(max + o.max, priority + o.priority, paid + o.paid, base + o.base)
}
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import java.util.function.Function
class EthereumLegacyFees(upstreams: EthereumMultistream, reader: EthereumReader, heightLimit: Int) :
EthereumFees(upstreams, reader, heightLimit) {
companion object {
private val log = LoggerFactory.getLogger(EthereumLegacyFees::class.java)
}
private val toGrpc: Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> = Function {
BlockchainOuterClass.EstimateFeeResponse.newBuilder()
.setEthereumExtended(
BlockchainOuterClass.EthereumExtFees.newBuilder()
.setMax(it.paid.amount.toString())
.setPriority(it.priority.amount.toString())
)
.build()
}
override fun extractFee(block: BlockJson<TransactionRefJson>, tx: TransactionJson): EthereumFee {
return EthereumFee(tx.gasPrice, tx.gasPrice, tx.gasPrice, Wei.ZERO)
}
override fun getResponseBuilder(): Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> {
return toGrpc
}
}

View File

@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Multistream
@@ -46,6 +47,12 @@ open class EthereumMultistream(
private val reader: EthereumReader = EthereumReader(this, this.caches, getMethodsFactory())
private val subscribe = EthereumSubscribe(this)
private val supportsEIP1559 = when (chain) {
Chain.ETHEREUM, Chain.TESTNET_ROPSTEN, Chain.TESTNET_GOERLI, Chain.TESTNET_RINKEBY -> true
else -> false
}
private val feeEstimation = if (supportsEIP1559) EthereumPriorityFees(this, reader, 256)
else EthereumLegacyFees(this, reader, 256)
init {
this.init()
@@ -133,4 +140,8 @@ open class EthereumMultistream(
open fun getSubscribe(): EthereumSubscribe {
return subscribe
}
override fun getFeeEstimation(): ChainFees {
return feeEstimation
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import java.util.function.Function
class EthereumPriorityFees(upstreams: EthereumMultistream, reader: EthereumReader, heightLimit: Int) :
EthereumFees(upstreams, reader, heightLimit) {
companion object {
private val log = LoggerFactory.getLogger(EthereumPriorityFees::class.java)
}
private val toGrpc: Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> =
Function {
BlockchainOuterClass.EstimateFeeResponse.newBuilder()
.setEthereumExtended(
BlockchainOuterClass.EthereumExtFees.newBuilder()
.setMax(it.max.amount.toString())
.setPriority(it.priority.amount.toString())
.setExpect(it.paid.amount.toString())
)
.build()
}
override fun extractFee(block: BlockJson<TransactionRefJson>, tx: TransactionJson): EthereumFee {
val baseFee = block.baseFeePerGas ?: Wei.ZERO
if (tx.type == 2) {
// an EIP-1559 Transaction provides Max and Priority fee
val paid = (baseFee + tx.maxPriorityFeePerGas).coerceAtMost(tx.maxFeePerGas)
return EthereumFee(tx.maxFeePerGas, tx.maxPriorityFeePerGas, paid, baseFee)
}
return EthereumFee(tx.gasPrice, (tx.gasPrice - baseFee).coerceAtLeast(Wei.ZERO), tx.gasPrice, baseFee)
}
override fun getResponseBuilder(): Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> {
return toGrpc
}
}

View File

@@ -125,7 +125,14 @@ open class EthereumReader(
)
}
fun txByHash(): Reader<TransactionId, TransactionJson> {
open fun blocksByHeightParsed(): Reader<Long, BlockJson<TransactionRefJson>> {
return TransformingReader(
blocksByHeightAsCont(),
extractBlock
)
}
open fun txByHash(): Reader<TransactionId, TransactionJson> {
return TransformingReader(
CompoundReader(
RekeyingReader(txHashToId, caches.getTxByHash()),