commonMain.fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lightning-kmp-jvm Show documentation
Show all versions of lightning-kmp-jvm Show documentation
A Kotlin Multiplatform implementation of the Lightning Network
package fr.acinq.lightning.blockchain.mempool
import fr.acinq.bitcoin.Transaction
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.blockchain.Feerates
import fr.acinq.lightning.blockchain.IClient
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
import fr.acinq.lightning.logging.LoggerFactory
import fr.acinq.lightning.logging.debug
import fr.acinq.lightning.logging.warning
import fr.acinq.lightning.utils.sat
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
class MempoolSpaceClient(val mempoolUrl: Url, loggerFactory: LoggerFactory) : IClient {
private val logger = loggerFactory.newLogger(this::class)
@OptIn(ExperimentalSerializationApi::class)
val client = HttpClient {
install(ContentNegotiation) {
json(json = Json {
prettyPrint = true
isLenient = true
explicitNulls = false // convert absent fields to null
ignoreUnknownKeys = true
})
}
install(DefaultRequest) {
url {
takeFrom(mempoolUrl)
}
}
}
suspend fun publish(tx: Transaction) {
val res = client.post("api/tx") {
contentType(ContentType.Text.Plain)
setBody(tx.toString())
}
if (!res.status.isSuccess()) {
logger.warning { "tx publish failed for txid=${tx.txid}: ${res.bodyAsText()}" }
}
}
/** Helper method to factor error handling and logging for api calls. */
private suspend fun tryWithLogs(f: suspend () -> T?): T? {
return kotlin.runCatching { f() }
.onFailure { logger.warning(it) { "mempool.space api error: " } }
.getOrNull()
}
suspend fun getTransaction(txId: TxId): Transaction? = tryWithLogs {
val res = client.get("api/tx/$txId/hex")
if (res.status.isSuccess()) {
Transaction.read(res.bodyAsText())
} else null
}
/**
* Returns a merkle inclusion proof for the transaction using Electrum's blockchain.transaction.get_merkle
* format.
* */
suspend fun getTransactionMerkleProof(txId: TxId): MempoolSpaceTransactionMerkleProofResponse? = tryWithLogs {
val res = client.get("api/tx/$txId/merkle-proof")
if (res.status.isSuccess()) {
val txStatus: MempoolSpaceTransactionMerkleProofResponse = res.body()
txStatus
} else null
}
/** Returns the spending status of a transaction output. */
suspend fun getOutspend(txId: TxId, outputIndex: Int): Transaction? = tryWithLogs {
logger.debug { "checking output $txId:$outputIndex" }
val res: MempoolSpaceOutspendResponse = client.get("api/tx/$txId/outspend/$outputIndex").body()
res.txid?.let { getTransaction(TxId(it)) }
}
/** Returns the height of the last block. */
suspend fun getBlockTipHeight(): Int? = tryWithLogs {
val res = client.get("api/blocks/tip/height")
if (res.status.isSuccess()) {
res.bodyAsText().toInt()
} else null
}
override suspend fun getConfirmations(txId: TxId): Int? = tryWithLogs {
val confirmedAtBlockHeight = getTransactionMerkleProof(txId)?.block_height
val currentBlockHeight = getBlockTipHeight()
when {
confirmedAtBlockHeight != null && currentBlockHeight != null -> currentBlockHeight - confirmedAtBlockHeight + 1
else -> null
}
}
override suspend fun getFeerates(): Feerates? = tryWithLogs {
val res: MempoolSpaceRecommendedFeerates = client.get("api/v1/fees/recommended").body()
Feerates(
minimum = FeeratePerByte(res.minimumFee.sat),
slow = FeeratePerByte(res.economyFee.sat),
medium = FeeratePerByte(res.hourFee.sat),
fast = FeeratePerByte(res.halfHourFee.sat),
fastest = FeeratePerByte(res.fastestFee.sat),
)
}
companion object {
val OfficialMempoolMainnet = Url("https://mempool.space")
val OfficialMempoolTestnet = Url("https://mempool.space/testnet/")
}
}
@Serializable
data class MempoolSpaceOutspendResponse(
val spent: Boolean,
val txid: String?,
val vin: Int?,
)
@Serializable
data class MempoolSpaceTransactionMerkleProofResponse(
val block_height: Int,
val pos: Int
)
@Serializable
data class MempoolSpaceRecommendedFeerates(
val fastestFee: Int,
val halfHourFee: Int,
val hourFee: Int,
val economyFee: Int,
val minimumFee: Int
)
© 2015 - 2024 Weber Informatics LLC | Privacy Policy