Merge pull request #137 from emeraldpay/feat/fee-estimation
This commit is contained in:
@@ -31,7 +31,7 @@ cglib-nodep = "cglib:cglib-nodep:3.3.0"
|
||||
|
||||
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
|
||||
|
||||
emerald-api = "io.emeraldpay:emerald-api:0.10.0"
|
||||
emerald-api = "io.emeraldpay:emerald-api:0.11.0"
|
||||
|
||||
equals-verifier = "nl.jqno.equalsverifier:equalsverifier:3.3"
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ service Blockchain {
|
||||
|
||||
rpc GetBalance (BalanceRequest) returns (stream AddressBalance) {}
|
||||
|
||||
/**
|
||||
* Fee Estimation service. The server tries to estimate a fair fee based on the last N blocks.
|
||||
*/
|
||||
rpc EstimateFee (EstimateFeeRequest) returns (EstimateFeeResponse) {}
|
||||
|
||||
rpc NativeCall (NativeCallRequest) returns (stream NativeCallReplyItem) {}
|
||||
rpc NativeSubscribe (NativeSubscribeRequest) returns (stream NativeSubscribeReplyItem) {}
|
||||
|
||||
@@ -172,4 +177,80 @@ message NotSelector {
|
||||
|
||||
message ExistsSelector {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for Fee Estimation Service
|
||||
*/
|
||||
message EstimateFeeRequest {
|
||||
// Target chain
|
||||
ChainRef chain = 1;
|
||||
// The way how the fee should be estimated
|
||||
FeeEstimationMode mode = 2;
|
||||
// How many blocks the server is supposed to use to estimate current fee. Note that the server may use value, depending on configuration
|
||||
uint32 blocks = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responset for Fee Estimation Service
|
||||
*/
|
||||
message EstimateFeeResponse {
|
||||
// May return different struct, depending on the blockchain
|
||||
oneof fee_type {
|
||||
// Standard Ethereum Fee, supported by majority of forks and by Ethereum Mainnet before EIP-1559
|
||||
EthereumStdFees ethereumStd = 1;
|
||||
// Ethereum Fee for EIP-1559 compatible forks
|
||||
EthereumExtFees ethereumExtended = 2;
|
||||
// Standard Bitcoin Fee
|
||||
BitcoinStdFees bitcoinStd = 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The mode of how the fee must be estimated
|
||||
*/
|
||||
enum FeeEstimationMode {
|
||||
INVALID = 0;
|
||||
// Average over last transaction in each block
|
||||
AVG_LAST = 1;
|
||||
// Average over transaction 5th from the end in each block
|
||||
AVG_T5 = 2;
|
||||
// Average over transaction 20th from the end in each block
|
||||
AVG_T20 = 3;
|
||||
// Average over transaction 50th from the end in each block
|
||||
AVG_T50 = 4;
|
||||
// Minimal fee that would be accepted by every last block
|
||||
MIN_ALWAYS = 5;
|
||||
// Average over transaction in the middle of each block
|
||||
AVG_MIDDLE = 6;
|
||||
// Average over transaction in head of each block. Note that for Bitcoin it doesn't count COINBASE tx as top tx.
|
||||
AVG_TOP = 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Ethereum Fee, supported by majority of forks and by Ethereum Mainnet before EIP-1559
|
||||
*/
|
||||
message EthereumStdFees {
|
||||
// Fee value in Wei
|
||||
string fee = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ethereum Fee for EIP-1559 compatible forks
|
||||
*/
|
||||
message EthereumExtFees {
|
||||
// Estimated fee that would be actually paid. I.e. it's the Base Fee + Priority Fee
|
||||
string expect = 1;
|
||||
// Priority Fee in Wei
|
||||
string priority = 2;
|
||||
// Max Fee value in Wei. Note that it only indicated current preference and actual Max may be significantly lower, depending on the usage scenario.
|
||||
string max = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Bitcoin Fee
|
||||
*/
|
||||
message BitcoinStdFees {
|
||||
// Fee in Satoshi per Kilobyte. Note that the actual fee calculation MUST divide it by 1024 at the last step to get a fair fee.
|
||||
string satPerKb = 1;
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class AccessHandlerGrpc(
|
||||
"NativeSubscribe" -> processNativeSubscribe(call, headers, next)
|
||||
"Describe" -> processDescribe(call, headers, next)
|
||||
"SubscribeStatus" -> processStatus(call, headers, next)
|
||||
"EstimateFee" -> processEstimateFee(call, headers, next)
|
||||
else -> {
|
||||
log.warn("unsupported method `{}`", method)
|
||||
next.startCall(call, headers)
|
||||
@@ -158,6 +159,18 @@ class AccessHandlerGrpc(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processEstimateFee(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(
|
||||
call, headers, next,
|
||||
EventsBuilder.EstimateFee() as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
open class StdCallListener<Req, EB : EventsBuilder.RequestReply<*, Req, *>>(
|
||||
val next: ServerCall.Listener<Req>,
|
||||
val builder: EB
|
||||
|
||||
@@ -136,6 +136,14 @@ class Events {
|
||||
val request: StreamRequestDetails
|
||||
) : ChainBase(blockchain, "Status", id, Channel.GRPC)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class EstimateFee(
|
||||
blockchain: Chain,
|
||||
id: UUID,
|
||||
val request: StreamRequestDetails,
|
||||
val estimateFee: EstimateFeeDetails
|
||||
) : ChainBase(blockchain, "EstimateFee", id, Channel.GRPC)
|
||||
|
||||
data class StreamRequestDetails(
|
||||
val id: UUID,
|
||||
val start: Instant,
|
||||
@@ -180,4 +188,9 @@ class Events {
|
||||
val asset: String,
|
||||
val address: String
|
||||
)
|
||||
|
||||
data class EstimateFeeDetails(
|
||||
val mode: String,
|
||||
val blocks: Int
|
||||
)
|
||||
}
|
||||
|
||||
@@ -452,4 +452,34 @@ class EventsBuilder {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EstimateFee :
|
||||
Base<EstimateFee>(),
|
||||
RequestReply<Events.EstimateFee, BlockchainOuterClass.EstimateFeeRequest, BlockchainOuterClass.EstimateFeeResponse> {
|
||||
|
||||
private var mode: String = "UNKNOWN"
|
||||
private var blocks: Int = 0
|
||||
|
||||
override fun getT(): EstimateFee {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onRequest(msg: BlockchainOuterClass.EstimateFeeRequest) {
|
||||
this.chain = Chain.byId(msg.chain.number)
|
||||
this.mode = msg.mode.name
|
||||
this.blocks = msg.blocks
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.EstimateFeeResponse): Events.EstimateFee {
|
||||
return Events.EstimateFee(
|
||||
blockchain = chain,
|
||||
request = requestDetails,
|
||||
id = UUID.randomUUID(),
|
||||
estimateFee = Events.EstimateFeeDetails(
|
||||
mode = mode,
|
||||
blocks = blocks
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,26 @@ class BlockchainRpc(
|
||||
}
|
||||
}
|
||||
|
||||
override fun estimateFee(request: Mono<BlockchainOuterClass.EstimateFeeRequest>): Mono<BlockchainOuterClass.EstimateFeeResponse> {
|
||||
return request
|
||||
.flatMap {
|
||||
val chain = Chain.byId(it.chainValue)
|
||||
val metrics = chainMetrics.get(chain)
|
||||
metrics.estimateFeeMetric.increment()
|
||||
val startTime = System.currentTimeMillis()
|
||||
estimateFee.estimateFee(it).doFinally {
|
||||
metrics.estimateFeeRespMetric.record(
|
||||
System.currentTimeMillis() - startTime,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
}
|
||||
}
|
||||
.doOnError { t ->
|
||||
log.error("Internal error during Fee Estimation", t)
|
||||
errorMetric.increment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
|
||||
describeMetric.increment()
|
||||
return describe.describe(request)
|
||||
@@ -224,5 +245,14 @@ class BlockchainRpc(
|
||||
.tag("chain", chain.chainCode)
|
||||
.publishPercentileHistogram()
|
||||
.register(Metrics.globalRegistry)
|
||||
val estimateFeeMetric = Counter.builder("request.grpc.request")
|
||||
.tag("type", "estimateFee")
|
||||
.tag("chain", chain.chainCode)
|
||||
.register(Metrics.globalRegistry)
|
||||
val estimateFeeRespMetric = Timer.builder("request.grpc.response")
|
||||
.tag("type", "estimateFee")
|
||||
.tag("chain", chain.chainCode)
|
||||
.publishPercentileHistogram()
|
||||
.register(Metrics.globalRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
|
||||
@@ -26,7 +26,6 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.emeraldpay.etherjar.rpc.json.ResponseJson
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufAllocator
|
||||
import io.netty.buffer.ByteBufInputStream
|
||||
@@ -57,7 +56,7 @@ import java.util.function.BiFunction
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Predicate
|
||||
|
||||
class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(this)
|
||||
List<PredefinedResponse> predefined = []
|
||||
@@ -66,15 +65,15 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
String id = "default"
|
||||
AtomicInteger calls = new AtomicInteger(0)
|
||||
|
||||
EthereumApiMock() {
|
||||
ApiReaderMock() {
|
||||
}
|
||||
|
||||
EthereumApiMock answerOnce(@NotNull String method, List<Object> params, Object result) {
|
||||
ApiReaderMock answerOnce(@NotNull String method, List<Object> params, Object result) {
|
||||
return answer(method, params, result, 1)
|
||||
}
|
||||
|
||||
EthereumApiMock answer(@NotNull String method, List<Object> params, Object result,
|
||||
Integer limit = null, Throwable exception = null) {
|
||||
ApiReaderMock answer(@NotNull String method, List<Object> params, Object result,
|
||||
Integer limit = null, Throwable exception = null) {
|
||||
predefined << new PredefinedResponse(method: method, params: params, result: result, limit: limit, exception: exception)
|
||||
return this
|
||||
}
|
||||
@@ -169,7 +168,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
}
|
||||
|
||||
class WebsocketApi {
|
||||
private final EthereumApiMock api
|
||||
private final ApiReaderMock api
|
||||
|
||||
private Sinks.Many<JsonRpcResponse> responses = Sinks
|
||||
.many()
|
||||
@@ -182,7 +181,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
private WebsocketOutboundMock outbound
|
||||
private WebsocketInboundMock inbound
|
||||
|
||||
WebsocketApi(EthereumApiMock api) {
|
||||
WebsocketApi(ApiReaderMock api) {
|
||||
this.api = api
|
||||
outbound = new WebsocketOutboundMock(api, responses)
|
||||
inbound = new WebsocketInboundMock(responses.asFlux(), jsonResponses.asFlux())
|
||||
@@ -260,10 +259,10 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
class WebsocketOutboundMock implements WebsocketOutbound {
|
||||
|
||||
private final EthereumApiMock api
|
||||
private final ApiReaderMock api
|
||||
private final Sinks.Many<JsonRpcResponse> responses
|
||||
|
||||
WebsocketOutboundMock(EthereumApiMock api, Sinks.Many<JsonRpcResponse> responses) {
|
||||
WebsocketOutboundMock(ApiReaderMock api, Sinks.Many<JsonRpcResponse> responses) {
|
||||
this.api = api
|
||||
this.responses = responses
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.reader.EmptyReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
@@ -39,15 +38,12 @@ import io.micrometer.core.instrument.MeterRegistry
|
||||
import io.micrometer.core.instrument.logging.LoggingMeterRegistry
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.temporal.TemporalUnit
|
||||
|
||||
class TestingCommons {
|
||||
|
||||
static EthereumApiMock api() {
|
||||
return new EthereumApiMock()
|
||||
static ApiReaderMock api() {
|
||||
return new ApiReaderMock()
|
||||
}
|
||||
|
||||
static EthereumUpstreamMock upstream() {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import kotlin.jvm.functions.Function1
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.function.Function
|
||||
|
||||
class AbstractChainFeesSpec extends Specification {
|
||||
|
||||
Function<List<String>, List<String>> extractTx = { it }
|
||||
|
||||
def "Iterates N last blocks"() {
|
||||
setup:
|
||||
def ups = Mock(Multistream) {
|
||||
1 * getHead() >> Mock(Head) {
|
||||
1 * it.getCurrentHeight() >> 234
|
||||
}
|
||||
}
|
||||
def fees = new TestChainFees(5, ups, Stub(Function1))
|
||||
when:
|
||||
def act = fees.usingBlocks(3).collectList().block(Duration.ofSeconds(1)).toSorted()
|
||||
then:
|
||||
act == [232L, 233L, 234L]
|
||||
}
|
||||
|
||||
def "Limits iteration to configured"() {
|
||||
setup:
|
||||
def ups = Mock(Multistream) {
|
||||
1 * getHead() >> Mock(Head) {
|
||||
1 * it.getCurrentHeight() >> 234
|
||||
}
|
||||
}
|
||||
def fees = new TestChainFees(3, ups, Stub(Function1))
|
||||
when:
|
||||
def act = fees.usingBlocks(5).collectList().block(Duration.ofSeconds(1)).toSorted()
|
||||
then:
|
||||
act == [232L, 233L, 234L]
|
||||
}
|
||||
|
||||
def "TxAtPos 0 for empty list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 0)
|
||||
|
||||
when:
|
||||
def act = txAt.get([])
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "TxAtPos 0 for single item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 0)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtPos 0 for many item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 0)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_3"
|
||||
}
|
||||
|
||||
def "TxAtPos 1 for many item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 1)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_2"
|
||||
}
|
||||
|
||||
def "TxAtPos 2 for many item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 2)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtPos 5 for 3 item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 5)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtBottom for empty list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtBottom<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get([])
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "TxAtBottom for single item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtBottom<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtBottom for multi item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtBottom<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_3"
|
||||
}
|
||||
|
||||
def "TxAtTop for empty list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtTop<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get([])
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "TxAtTop for single item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtTop<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtTop for multi item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtTop<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtMiddle for empty list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get([])
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "TxAtMiddle for single item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1"])
|
||||
|
||||
then:
|
||||
act == "t_1"
|
||||
}
|
||||
|
||||
def "TxAtMiddle for 3 item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3"])
|
||||
|
||||
then:
|
||||
act == "t_2"
|
||||
}
|
||||
|
||||
def "TxAtMiddle for 4 item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3", "t_4"])
|
||||
|
||||
then:
|
||||
act == "t_2" || act == "t_3"
|
||||
}
|
||||
|
||||
def "TxAtMiddle for 5 item list"() {
|
||||
setup:
|
||||
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
|
||||
|
||||
when:
|
||||
def act = txAt.get(["t_1", "t_2", "t_3", "t_4", "t_5"])
|
||||
|
||||
then:
|
||||
act == "t_3"
|
||||
}
|
||||
|
||||
class TestChainFees extends AbstractChainFees {
|
||||
|
||||
TestChainFees(int heightLimit, @NotNull Multistream upstreams, @NotNull Function1 extractTx) {
|
||||
super(heightLimit, upstreams, extractTx)
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono readFeesAt(long height, @NotNull TxAt selector) {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
Function<Flux, Mono> feeAggregation(@NotNull Mode mode) {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
Function getResponseBuilder() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -225,5 +225,10 @@ class MultistreamSpec extends Specification {
|
||||
public <T extends Upstream> T cast(Class<T> selfType) {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
ChainFees getFeeEstimation() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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.dshackle.upstream.ChainFees
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class BitcoinFeesSpec extends Specification {
|
||||
|
||||
def block3 = [
|
||||
hash: "3300000000033327aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506",
|
||||
tx : [
|
||||
"d5a67d8f99ad05ece282bc8714da55cbd2266d9d7bae98fd5dda3b55d1696fb8",
|
||||
"e17eca6b595da9c1c7b883fe505184d7ff586315d5b8a0c476198f0d49a75ad3",
|
||||
"1735dfef80bcfdc443943dcbc8d26a4133da87c5c81d9e2a1e0a0e5f59d1ef96",
|
||||
"c83c99e50946a6d30bc344a24401ad0ff0598425d73a5f74d6545e15ae588321",
|
||||
"0d4f85f840f09f317d0b202725acee868b2e969078bb6a6b89fc3e0bc1216c6a",
|
||||
"4d56b1a0992a3fa3132083ce29e203165ef4768f028d98a738da79dd26ff91e2",
|
||||
]
|
||||
]
|
||||
def block2 = [
|
||||
hash: "2200000000022227aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506",
|
||||
tx : [
|
||||
"87cc7d37851af41da60a5d14e40983d0a6750ffcdf1f2ae1291bfc1e712f0712",
|
||||
"3bde80635c62e81ba86d68a1c78ac47b1a02d8ef334fdf6a7b6e8acc0a475f20",
|
||||
"2f4f413c07ac072918e12fcac4afd8d30a76dfe49ac68d46d582c3a130772734",
|
||||
"f29ecfa5b692cfc46b1eae29416a35782657f0bde2ae44acde882e477e8624d8",
|
||||
]
|
||||
]
|
||||
|
||||
def tx4D5 = [
|
||||
hash : "4d56b1a0992a3fa3132083ce29e203165ef4768f028d98a738da79dd26ff91e2",
|
||||
size : 223,
|
||||
vsize: 142,
|
||||
vin : [
|
||||
[txid: "3bde80635c62e81ba86d68a1c78ac47b1a02d8ef334fdf6a7b6e8acc0a475f20", vout: 1]
|
||||
],
|
||||
vout : [
|
||||
["value": 0.00029163, "n": 0],
|
||||
["value": 0.00144221, "n": 1]
|
||||
]
|
||||
]
|
||||
def txF29 = [
|
||||
hash : "f29ecfa5b692cfc46b1eae29416a35782657f0bde2ae44acde882e477e8624d8",
|
||||
size : 2900,
|
||||
vsize: 1366,
|
||||
vin : [
|
||||
[txid: "37e19fad6c3e5fafc431ee731428452dc01f56b0bc4d9fe876dcee58384934ec", "vout": 0],
|
||||
[txid: "3da079b10451ba1cde42d9a0f485e79151e5d4216930561227be5471921af17f", "vout": 0]
|
||||
],
|
||||
vout : [
|
||||
["value": 1.20100000, "n": 0],
|
||||
["value": 0.00210984, "n": 1]
|
||||
]
|
||||
]
|
||||
|
||||
def tx3BD = [
|
||||
hash: "3bde80635c62e81ba86d68a1c78ac47b1a02d8ef334fdf6a7b6e8acc0a475f20",
|
||||
size: 292,
|
||||
vin : [
|
||||
],
|
||||
vout: [
|
||||
["value": 0.00400000, "n": 0],
|
||||
["value": 0.00200000, "n": 1],
|
||||
]
|
||||
]
|
||||
def tx37E = [
|
||||
hash : "37e19fad6c3e5fafc431ee731428452dc01f56b0bc4d9fe876dcee58384934ec",
|
||||
size : 292,
|
||||
vsize: 200,
|
||||
vin : [
|
||||
],
|
||||
vout : [
|
||||
["value": 1.00000000, "n": 0],
|
||||
]
|
||||
]
|
||||
def tx3DA = [
|
||||
hash : "3da079b10451ba1cde42d9a0f485e79151e5d4216930561227be5471921af17f",
|
||||
size : 297,
|
||||
vsize: 201,
|
||||
vin : [
|
||||
],
|
||||
vout : [
|
||||
["value": 0.21000000, "n": 0],
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def "fetch tx amount"() {
|
||||
setup:
|
||||
def reader = Mock(BitcoinReader) {
|
||||
1 * it.getTx("4d56b1a0992a3fa3132083ce29e203165ef4768f028d98a738da79dd26ff91e2") >> Mono.just(tx4D5)
|
||||
}
|
||||
def fees = new BitcoinFees(Stub(BitcoinMultistream), reader, 3)
|
||||
|
||||
when:
|
||||
def amount = fees.getTxOutAmount("4d56b1a0992a3fa3132083ce29e203165ef4768f028d98a738da79dd26ff91e2", 0)
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
amount == 29163
|
||||
}
|
||||
|
||||
def "extract amounts"() {
|
||||
setup:
|
||||
def fees = new BitcoinFees(Stub(BitcoinMultistream), Stub(BitcoinReader), 3)
|
||||
|
||||
when:
|
||||
def act = fees.extractVOuts(txF29)
|
||||
then:
|
||||
act == [120100000L, 210984L]
|
||||
}
|
||||
|
||||
def "extract vsize when available"() {
|
||||
setup:
|
||||
def fees = new BitcoinFees(Stub(BitcoinMultistream), Stub(BitcoinReader), 3)
|
||||
|
||||
when:
|
||||
def act = fees.extractSize(tx4D5)
|
||||
then:
|
||||
act == 142
|
||||
}
|
||||
|
||||
def "extract size when vsize unavailable"() {
|
||||
setup:
|
||||
def fees = new BitcoinFees(Stub(BitcoinMultistream), Stub(BitcoinReader), 3)
|
||||
|
||||
when:
|
||||
def act = fees.extractSize(tx3BD)
|
||||
then:
|
||||
act == 292
|
||||
}
|
||||
|
||||
def "calculates fee"() {
|
||||
setup:
|
||||
def reader = Mock(BitcoinReader) {
|
||||
1 * it.getTx("3bde80635c62e81ba86d68a1c78ac47b1a02d8ef334fdf6a7b6e8acc0a475f20") >> Mono.just(tx3BD)
|
||||
}
|
||||
def fees = new BitcoinFees(Stub(BitcoinMultistream), reader, 3)
|
||||
when:
|
||||
def act = fees.calculateFee(tx4D5).block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == 200000 - (29163 + 144221)
|
||||
}
|
||||
|
||||
def "calculates fee with multiple inputs"() {
|
||||
setup:
|
||||
def reader = Mock(BitcoinReader) {
|
||||
1 * it.getTx("37e19fad6c3e5fafc431ee731428452dc01f56b0bc4d9fe876dcee58384934ec") >> Mono.just(tx37E)
|
||||
1 * it.getTx("3da079b10451ba1cde42d9a0f485e79151e5d4216930561227be5471921af17f") >> Mono.just(tx3DA)
|
||||
}
|
||||
def fees = new BitcoinFees(Stub(BitcoinMultistream), reader, 3)
|
||||
when:
|
||||
def act = fees.calculateFee(txF29).block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == (100000000 + 21000000) - (120100000 + 210984)
|
||||
}
|
||||
|
||||
def "get average bottom fee"() {
|
||||
setup:
|
||||
def ups = Mock(BitcoinMultistream) {
|
||||
_ * getHead() >> Mock(Head) {
|
||||
_ * getCurrentHeight() >> 100
|
||||
}
|
||||
}
|
||||
def reader = Mock(BitcoinReader) {
|
||||
1 * it.getBlock(100) >> Mono.just(block3)
|
||||
1 * it.getBlock(99) >> Mono.just(block2)
|
||||
// last txes on those blocks
|
||||
1 * it.getTx("4d56b1a0992a3fa3132083ce29e203165ef4768f028d98a738da79dd26ff91e2") >> Mono.just(tx4D5)
|
||||
1 * it.getTx("f29ecfa5b692cfc46b1eae29416a35782657f0bde2ae44acde882e477e8624d8") >> Mono.just(txF29)
|
||||
// their inputs
|
||||
1 * it.getTx("3bde80635c62e81ba86d68a1c78ac47b1a02d8ef334fdf6a7b6e8acc0a475f20") >> Mono.just(tx3BD)
|
||||
1 * it.getTx("37e19fad6c3e5fafc431ee731428452dc01f56b0bc4d9fe876dcee58384934ec") >> Mono.just(tx37E)
|
||||
1 * it.getTx("3da079b10451ba1cde42d9a0f485e79151e5d4216930561227be5471921af17f") >> Mono.just(tx3DA)
|
||||
}
|
||||
def fees = new BitcoinFees(ups, reader, 3)
|
||||
|
||||
when:
|
||||
def act = fees.estimate(ChainFees.Mode.AVG_LAST, 2).block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act.hasBitcoinStd()
|
||||
// first tx fee: 187.43661971830985915493
|
||||
// second tx fee: 504.40409956076134699854
|
||||
// average is 345
|
||||
// but if we calculate original fees per KB it's 354222
|
||||
act.bitcoinStd.satPerKb == "354222"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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.dshackle.test.ApiReaderMock
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class BitcoinReaderSpec extends Specification {
|
||||
|
||||
def "gets block by height"() {
|
||||
setup:
|
||||
def block = [
|
||||
hash: "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506",
|
||||
tx : []
|
||||
]
|
||||
def api = new ApiReaderMock()
|
||||
api.answerOnce("getblockhash", [100000], "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506")
|
||||
api.answerOnce("getblock", ["000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506"], block)
|
||||
def ups = Mock(BitcoinMultistream) {
|
||||
_ * it.getDirectApi(_) >> Mono.just(api)
|
||||
}
|
||||
def reader = new BitcoinReader(ups, Stub(Head), null)
|
||||
|
||||
when:
|
||||
def act = reader.getBlock(100000).block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act == block
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.etherjar.domain.Wei
|
||||
import io.emeraldpay.etherjar.rpc.json.BlockJson
|
||||
import io.emeraldpay.etherjar.rpc.json.TransactionJson
|
||||
import spock.lang.Specification
|
||||
|
||||
class EthereumLegacyFeesSpec extends Specification {
|
||||
|
||||
def "Extract fee from"() {
|
||||
setup:
|
||||
def block = new BlockJson()
|
||||
// 0x75cc01873a9818bf426a8b23d83450bf18530a822fd4fe9e86a416a5554176a6
|
||||
def tx = new TransactionJson().tap {
|
||||
it.gasPrice = Wei.ofUnits(8, Wei.Unit.GWEI)
|
||||
}
|
||||
|
||||
def fees = new EthereumLegacyFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
|
||||
when:
|
||||
def act = fees.extractFee(block, tx)
|
||||
then:
|
||||
act.priority == Wei.ofUnits(8, Wei.Unit.GWEI)
|
||||
act.max == Wei.ofUnits(8, Wei.Unit.GWEI)
|
||||
act.base == Wei.ZERO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.ChainFees
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.etherjar.domain.TransactionId
|
||||
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 reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class EthereumPriorityFeesSpec extends Specification {
|
||||
|
||||
def "Extract fee from EIP1559 tx"() {
|
||||
setup:
|
||||
// 13756007
|
||||
def block = new BlockJson().tap {
|
||||
it.baseFeePerGas = new Wei(104197355513)
|
||||
}
|
||||
// 0x5da50f35a51e56ecd4313417b1c30f9c088222f3f8763701effe14f3dd18b6cc
|
||||
def tx = new TransactionJson().tap {
|
||||
it.type = 2
|
||||
it.maxFeePerGas = Wei.ofUnits(999, Wei.Unit.GWEI)
|
||||
it.maxPriorityFeePerGas = Wei.ofUnits(5.0001, Wei.Unit.GWEI)
|
||||
}
|
||||
|
||||
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
|
||||
when:
|
||||
def act = fees.extractFee(block, tx)
|
||||
then:
|
||||
act.priority == Wei.ofUnits(5.0001, Wei.Unit.GWEI)
|
||||
act.max == Wei.ofUnits(999, Wei.Unit.GWEI)
|
||||
act.base == new Wei(104197355513)
|
||||
}
|
||||
|
||||
def "Extract fee from legacy tx"() {
|
||||
setup:
|
||||
// 13756007
|
||||
def block = new BlockJson().tap {
|
||||
it.baseFeePerGas = new Wei(104197355513)
|
||||
}
|
||||
// 0x1f507982bef0f11a8304287d41f228b5f1dda1114a446ee781c3d95ef4a7b891
|
||||
def tx = new TransactionJson().tap {
|
||||
it.type = 0
|
||||
// 109.564020111 Gwei
|
||||
it.gasPrice = Wei.from("0x198286458f")
|
||||
}
|
||||
|
||||
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
|
||||
when:
|
||||
def act = fees.extractFee(block, tx)
|
||||
then:
|
||||
// as difference between base minimum and actually paid
|
||||
act.priority == Wei.ofUnits(5.366664598, Wei.Unit.GWEI)
|
||||
act.max == Wei.from("0x198286458f")
|
||||
act.base == new Wei(104197355513)
|
||||
}
|
||||
|
||||
def "Calculates average fee"() {
|
||||
setup:
|
||||
def inputs = [
|
||||
new EthereumFees.EthereumFee(Wei.ofEthers(1), Wei.ofEthers(0.5), Wei.ofEthers(0.75), Wei.ofEthers(0.5)),
|
||||
new EthereumFees.EthereumFee(Wei.ofEthers(0.75), Wei.ofEthers(0.5), Wei.ofEthers(0.75), Wei.ofEthers(0.5)),
|
||||
new EthereumFees.EthereumFee(Wei.ofEthers(0.6), Wei.ofEthers(0.2), Wei.ofEthers(0.6), Wei.ofEthers(0.5)),
|
||||
]
|
||||
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
|
||||
when:
|
||||
def act = Flux.fromIterable(inputs)
|
||||
.transform(fees.feeAggregation(ChainFees.Mode.AVG_LAST))
|
||||
.next().block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act.priority == Wei.ofEthers(0.4) // 0.5 + 0.5 + 0.2
|
||||
act.paid == Wei.ofEthers(0.7) // 0.75 + 0.75 + 0.6
|
||||
}
|
||||
|
||||
def "Estimate"() {
|
||||
setup:
|
||||
def block1 = new BlockJson().tap {
|
||||
it.baseFeePerGas = new Wei(92633661632)
|
||||
it.transactions = [
|
||||
new TransactionRefJson(TransactionId.from("0x00000000fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
|
||||
new TransactionRefJson(TransactionId.from("0x11111111fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
|
||||
new TransactionRefJson(TransactionId.from("0x22222222fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
|
||||
]
|
||||
}
|
||||
def block2 = new BlockJson().tap {
|
||||
it.baseFeePerGas = new Wei(104197355513)
|
||||
it.transactions = [
|
||||
new TransactionRefJson(TransactionId.from("0x33333333fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
|
||||
new TransactionRefJson(TransactionId.from("0x44444444fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
|
||||
new TransactionRefJson(TransactionId.from("0x55555555fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
|
||||
]
|
||||
}
|
||||
def tx1 = new TransactionJson().tap {
|
||||
it.type = 2
|
||||
it.maxFeePerGas = Wei.ofUnits(150, Wei.Unit.GWEI)
|
||||
it.maxPriorityFeePerGas = Wei.ofUnits(3, Wei.Unit.GWEI)
|
||||
}
|
||||
def tx2 = new TransactionJson().tap {
|
||||
it.type = 2
|
||||
it.maxFeePerGas = Wei.ofUnits(200, Wei.Unit.GWEI)
|
||||
it.maxPriorityFeePerGas = Wei.ofUnits(6, Wei.Unit.GWEI)
|
||||
}
|
||||
|
||||
def ups = Mock(EthereumMultistream) {
|
||||
1 * getHead() >> Mock(Head) {
|
||||
1 * getCurrentHeight() >> 13756007
|
||||
}
|
||||
}
|
||||
def reader = Mock(EthereumReader) {
|
||||
_ * it.blocksByHeightParsed() >> Mock(Reader) {
|
||||
1 * it.read(13756006) >> Mono.just(block1)
|
||||
1 * it.read(13756007) >> Mono.just(block2)
|
||||
}
|
||||
_ * it.txByHash() >> Mock(Reader) {
|
||||
1 * it.read(TransactionId.from("0x22222222fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")) >> Mono.just(tx1)
|
||||
1 * it.read(TransactionId.from("0x55555555fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")) >> Mono.just(tx2)
|
||||
}
|
||||
}
|
||||
def fees = new EthereumPriorityFees(ups, reader, 10)
|
||||
when:
|
||||
def act = fees.estimate(ChainFees.Mode.AVG_LAST, 2).block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act.hasEthereumExtended()
|
||||
act.ethereumExtended.priority == "4500000000"
|
||||
act.ethereumExtended.max == "175000000000"
|
||||
act.ethereumExtended.expect == "102915508572"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user