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

@@ -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
}

View File

@@ -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() {

View File

@@ -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
}
}
}

View File

@@ -225,5 +225,10 @@ class MultistreamSpec extends Specification {
public <T extends Upstream> T cast(Class<T> selfType) {
return this
}
@Override
ChainFees getFeeEstimation() {
return null
}
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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"
}
}