solution: initial code for fee estimation
This commit is contained in:
@@ -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