Skip to content

Commit

Permalink
Retrieve onchain feerate when we connect to an electrum server
Browse files Browse the repository at this point in the history
When we connect to an electrum sever we perform a "handshake" that includes exchanging protocol version messages and
retrieving the server's current tip, and now we also retrieve onchain fees.
  • Loading branch information
sstone committed Jun 27, 2023
1 parent 79f45c8 commit 48ae010
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package fr.acinq.lightning.blockchain.electrum

import fr.acinq.bitcoin.*
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.blockchain.fee.OnChainFeerates
import fr.acinq.lightning.io.TcpSocket
import fr.acinq.lightning.io.linesFlow
import fr.acinq.lightning.io.send
Expand All @@ -23,7 +26,7 @@ sealed interface ElectrumClientCommand {
sealed interface ElectrumConnectionStatus {
data class Closed(val reason: TcpSocket.IOException?) : ElectrumConnectionStatus
object Connecting : ElectrumConnectionStatus
data class Connected(val version: ServerVersionResponse, val height: Int, val header: BlockHeader) : ElectrumConnectionStatus
data class Connected(val version: ServerVersionResponse, val height: Int, val header: BlockHeader, val onchainFeeRates: OnChainFeerates) : ElectrumConnectionStatus
}

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -139,17 +142,36 @@ class ElectrumClient(
}

val flow = socket.linesFlow().map { json.decodeFromString(ElectrumResponseDeserializer, it) }

val version = ServerVersion()
sendRequest(version, 0)
val rpcFlow = flow.filterIsInstance<Either.Right<Nothing, JsonRPCResponse>>().map { it.value }
val theirVersion = parseJsonResponse(version, rpcFlow.first())
require(theirVersion is ServerVersionResponse) { "invalid server version response $theirVersion" }
logger.info { "server version $theirVersion" }

sendRequest(HeaderSubscription, 0)
val header = parseJsonResponse(HeaderSubscription, rpcFlow.first())
require(header is HeaderSubscriptionResponse) { "invalid header subscription response $header" }

suspend fun estimateFee(confirmations: Int): EstimateFeeResponse {
val request = EstimateFees(confirmations)
sendRequest(request, 0)
val response = parseJsonResponse(request, rpcFlow.first())
require(response is EstimateFeeResponse) { "invalid estimatefee response $response" }
return response
}

val fees = listOf(estimateFee(2), estimateFee(6), estimateFee(18), estimateFee(144))
logger.info { "onchain fees $fees" }
val feeRates = OnChainFeerates(
fundingFeerate = fees[3].feerate ?: FeeratePerKw(FeeratePerByte(2.sat)),
mutualCloseFeerate = fees[2].feerate ?: FeeratePerKw(FeeratePerByte(10.sat)),
claimMainFeerate = fees[1].feerate ?: FeeratePerKw(FeeratePerByte(20.sat)),
fastFeerate = fees[0].feerate ?: FeeratePerKw(FeeratePerByte(50.sat))
)
_notifications.emit(header)
_connectionStatus.value = ElectrumConnectionStatus.Connected(theirVersion, header.blockHeight, header.header)
_connectionStatus.value = ElectrumConnectionStatus.Connected(theirVersion, header.blockHeight, header.header, feeRates)
logger.info { "server tip $header" }

// pending requests map
Expand Down
28 changes: 5 additions & 23 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,11 @@ class Peer(
}
}
launch {
watcher.client.connectionState.filter { it == Connection.ESTABLISHED }.collect {
// onchain fees are retrieved punctually, when electrum status moves to Connection.ESTABLISHED
// since the application is not running most of the time, and when it is, it will be only for a few minutes, this is good enough.
// (for a node that is online most of the time things would be different and we would need to re-evaluate onchain fee estimates on a regular basis)
updateEstimateFees()
watcher.client.connectionStatus.filterIsInstance<ElectrumConnectionStatus.Connected>().collect {
// Onchain fees are retrieved once when we establish a connection to an electrum server.
// It is acceptable since the application will typically not be running more than a few minutes at a time.
// (for a node that is online most of the time things would be different, and we would need to re-evaluate onchain fee estimates on a regular basis)
onChainFeeratesFlow.value = it.onchainFeeRates
}
}
launch {
Expand Down Expand Up @@ -257,24 +257,6 @@ class Peer(
}
}

private suspend fun updateEstimateFees() {
watcher.client.connectionState.filter { it == Connection.ESTABLISHED }.first()
val sortedFees = listOf(
watcher.client.estimateFees(2),
watcher.client.estimateFees(6),
watcher.client.estimateFees(18),
watcher.client.estimateFees(144),
)
logger.info { "on-chain fees: $sortedFees" }
// TODO: If some feerates are null, we may implement a retry
onChainFeeratesFlow.value = OnChainFeerates(
fundingFeerate = sortedFees[3].feerate ?: FeeratePerKw(FeeratePerByte(2.sat)),
mutualCloseFeerate = sortedFees[2].feerate ?: FeeratePerKw(FeeratePerByte(10.sat)),
claimMainFeerate = sortedFees[1].feerate ?: FeeratePerKw(FeeratePerByte(20.sat)),
fastFeerate = sortedFees[0].feerate ?: FeeratePerKw(FeeratePerByte(50.sat))
)
}

fun connect() {
if (connectionState.value is Connection.CLOSED) establishConnection()
else logger.warning { "Peer is already connecting / connected" }
Expand Down

0 comments on commit 48ae010

Please sign in to comment.