solution: initial code for fee estimation
This commit is contained in:
@@ -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)
|
||||
|
||||
38
src/main/kotlin/io/emeraldpay/dshackle/rpc/EstimateFee.kt
Normal file
38
src/main/kotlin/io/emeraldpay/dshackle/rpc/EstimateFee.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainFees.kt
Normal file
35
src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainFees.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user