Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,81 @@ Example response:
}
```


Historical fee estimates at:

```
GET /historical_fee?timestamp=<unix_timestamp>
```

Example response:
```json

{
"mempool_update_time" : "2025-08-16T14:05:44.854Z",
"estimates" : {
"3" : {
"probabilities" : {
"0.05" : {
"fee_rate" : 1.0
},
"0.20" : {
"fee_rate" : 1.0
},
"0.50" : {
"fee_rate" : 1.0
},
"0.80" : {
"fee_rate" : 1.0
},
"0.95" : {
"fee_rate" : 1.0
}
}
},
"6" : {
"probabilities" : {
"0.05" : {
"fee_rate" : 1.0
},
"0.20" : {
"fee_rate" : 1.0
},
"0.50" : {
"fee_rate" : 1.0
},
"0.80" : {
"fee_rate" : 1.0
},
"0.95" : {
"fee_rate" : 1.0
}
}
},
"9" : {
"probabilities" : {
"0.05" : {
"fee_rate" : 1.0
},
"0.20" : {
"fee_rate" : 1.0
},
"0.50" : {
"fee_rate" : 1.0
},
"0.80" : {
"fee_rate" : 1.0
},
"0.95" : {
"fee_rate" : 1.0
}
}
}
}
}

```

## Local Development
If you'd like to use a local version of [augur](https://github.com/block/bitcoin-augur) within your reference implementation:
- Within augur, run `bin/gradle shadowJar` to build a fat jar of augur.
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ dependencies {
testImplementation(libs.ktor.client.core)
testImplementation(libs.ktor.client.cio)
testImplementation(libs.ktor.client.content.negotiation)

// MockK for mocking dependencies
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0")
}

// Apply a specific Java toolchain to ease working on different environments.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 Block, 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 xyz.block.augurref.api

import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import org.slf4j.LoggerFactory
import xyz.block.augurref.service.MempoolCollector

/**
* Configure historical fee estimate endpoint
*/
fun Route.configureHistoricalFeesEndpoint(mempoolCollector: MempoolCollector) {
val logger = LoggerFactory.getLogger("xyz.block.augurref.api.HistoricalFeeEstimateEndpoint")

get("/historical_fee") {
logger.info("Received request for historical fee estimates")

// Extract unix timestamp param from query parameters
val timestampParam = call.request.queryParameters["timestamp"]

if (timestampParam == null) {
call.respondText(
"timestamp parameter is required",
status = HttpStatusCode.BadRequest,
contentType = ContentType.Text.Plain,
)
return@get
}
val timestamp = timestampParam.toLongOrNull()

// Fetch historical fee estimate based on unix timestamp
val feeEstimate = if (timestamp != null) {
logger.info("Fetching historical fee estimate for timestamp: $timestamp")
mempoolCollector.getFeeEstimateForTimestamp(timestamp)
} else {
logger.warn("timestamp is null")
call.respondText(
"Failed to parse timestamp, please input a unix timestamp",
status = HttpStatusCode.BadRequest,
contentType = ContentType.Text.Plain,
)
return@get
}

if (feeEstimate == null) {
logger.warn("No historical fee estimates available for $timestamp")
call.respondText(
"No historical fee estimates available for $timestamp",
status = HttpStatusCode.ServiceUnavailable,
contentType = ContentType.Text.Plain,
)
} else {
logger.info("Transforming historical fee estimates for response")
val response = transformFeeEstimate(feeEstimate)
logger.debug("Returning historical fee estimates with ${response.estimates.size} targets")
call.respond(response)
}
}
}
2 changes: 2 additions & 0 deletions app/src/main/kotlin/xyz/block/augurref/server/HttpServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.routing
import org.slf4j.LoggerFactory
import xyz.block.augurref.api.configureFeesEndpoint
import xyz.block.augurref.api.configureHistoricalFeesEndpoint
import xyz.block.augurref.config.ServerConfig
import xyz.block.augurref.service.MempoolCollector

Expand Down Expand Up @@ -58,6 +59,7 @@ class HttpServer(
// Configure routes
routing {
configureFeesEndpoint(mempoolCollector)
configureHistoricalFeesEndpoint(mempoolCollector)
}
}.start(wait = false)

Expand Down
32 changes: 32 additions & 0 deletions app/src/main/kotlin/xyz/block/augurref/service/MempoolCollector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import xyz.block.augurref.bitcoin.BitcoinRpcClient
import xyz.block.augurref.persistence.MempoolPersistence
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.concurrent.atomic.AtomicReference
import kotlin.concurrent.fixedRateTimer

Expand Down Expand Up @@ -71,6 +72,37 @@ class MempoolCollector(
return latestFeeEstimate.get()
}

/**
* Get the fee estimate for specific date
*/
fun getFeeEstimateForTimestamp(unixTimestamp: Long): FeeEstimate? {
val dateTime = Instant
.ofEpochSecond(unixTimestamp)
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime()
// Fetch the last day's snapshots
logger.debug("Fetching snapshots from the last day")
val lastDaySnapshots = persistence.getSnapshots(
dateTime.minusDays(1),
dateTime,
)
logger.debug("Retrieved ${lastDaySnapshots.size} snapshots from the last day")

if (lastDaySnapshots.isNotEmpty()) {
// Calculate fee estimate for x blocks
logger.debug("Calculating fee estimates")
val newFeeEstimate = feeEstimator.calculateEstimates(lastDaySnapshots)
return newFeeEstimate
} else {
logger.warn("No snapshots available for fee estimation")
}
return FeeEstimate(
estimates = emptyMap(),
timestamp = dateTime.atZone(ZoneId.of("UTC")).toInstant(),
)
}

/**
* Get the latest fee estimate for block target
*/
Expand Down
Loading