Swap-in wallet functionalities
This commit is contained in:
parent
2e22c0b80c
commit
d7333c87ad
@ -8,7 +8,6 @@ import fr.acinq.bitcoin.utils.Either
|
||||
import fr.acinq.bitcoin.utils.toEither
|
||||
import fr.acinq.lightning.BuildVersions
|
||||
import fr.acinq.lightning.Lightning.randomBytes32
|
||||
import fr.acinq.lightning.Lightning.randomKey
|
||||
import fr.acinq.lightning.NodeParams
|
||||
import fr.acinq.lightning.bin.db.SqlitePaymentsDb
|
||||
import fr.acinq.lightning.bin.db.WalletPaymentId
|
||||
@ -22,6 +21,7 @@ import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
|
||||
import fr.acinq.lightning.channel.states.Closed
|
||||
import fr.acinq.lightning.channel.states.Closing
|
||||
import fr.acinq.lightning.channel.states.ClosingFeerates
|
||||
import fr.acinq.lightning.channel.states.Normal
|
||||
import fr.acinq.lightning.io.Peer
|
||||
import fr.acinq.lightning.io.WrappedChannelCommand
|
||||
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||
@ -43,7 +43,12 @@ import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -114,7 +119,10 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
||||
.filterNot { it is Closing || it is Closed }
|
||||
.map { it.commitments.active.first().availableBalanceForSend(it.commitments.params, it.commitments.changes) }
|
||||
.sum().truncateToSatoshi()
|
||||
call.respond(Balance(balance, nodeParams.feeCredit.value))
|
||||
val swapInBalance = peer.swapInWallet.wallet.walletStateFlow
|
||||
.map { it.totalBalance }
|
||||
.distinctUntilChanged().first()
|
||||
call.respond(Balance(balance, nodeParams.feeCredit.value, swapInBalance))
|
||||
}
|
||||
get("listchannels") {
|
||||
call.respond(peer.channels.values.toList())
|
||||
@ -181,6 +189,15 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
||||
call.respond(OutgoingPayment(it))
|
||||
} ?: call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
delete("payments/incoming/{paymentHash}") {
|
||||
val paymentHash = call.parameters.getByteVector32("paymentHash")
|
||||
val success = paymentDb.removeIncomingPayment(paymentHash)
|
||||
if (success) {
|
||||
call.respondText("Payment successfully deleted", status = HttpStatusCode.OK)
|
||||
} else {
|
||||
call.respondText("Payment not found or failed to delete", status = HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
post("payinvoice") {
|
||||
val formParameters = call.receiveParameters()
|
||||
val overrideAmount = formParameters["amountSat"]?.let { it.toLongOrNull() ?: invalidType("amountSat", "integer") }?.sat?.toMilliSatoshi()
|
||||
@ -214,6 +231,84 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
||||
val offer = formParameters.getOffer("offer")
|
||||
call.respond(offer)
|
||||
}
|
||||
get("/getfinaladdress"){
|
||||
val finalAddress = peer.finalWallet.finalAddress
|
||||
call.respond(finalAddress)
|
||||
}
|
||||
get("/getswapinaddress") {
|
||||
try {
|
||||
val swapInWalletStateFlow = peer.swapInWallet.wallet.walletStateFlow
|
||||
swapInWalletStateFlow
|
||||
.map { it.lastDerivedAddress }
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.collect { (address, derived) ->
|
||||
call.respond(SwapInAddress(address, derived.index))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, "Error fetching swap-in address")
|
||||
}
|
||||
}
|
||||
get("/finalwalletbalance") {
|
||||
try {
|
||||
val currentBlockHeight: Long = peer.currentTipFlow.filterNotNull().first().toLong()
|
||||
val walletStateFlow = peer.finalWallet.wallet.walletStateFlow
|
||||
val utxosFlow = walletStateFlow.map { walletState ->
|
||||
walletState.utxos.groupBy { utxo ->
|
||||
val confirmations = currentBlockHeight - utxo.blockHeight + 1
|
||||
when {
|
||||
confirmations < 1 -> "unconfirmed"
|
||||
confirmations < 3 -> "weaklyConfirmed"
|
||||
else -> "deeplyConfirmed"
|
||||
}
|
||||
}.mapValues { entry ->
|
||||
entry.value.sumOf { it.amount.toLong() }
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
val balancesByConfirmation = utxosFlow.first()
|
||||
val response = WalletBalance(
|
||||
unconfirmed = balancesByConfirmation["unconfirmed"] ?: 0L,
|
||||
weaklyConfirmed = balancesByConfirmation["weaklyConfirmed"] ?: 0L,
|
||||
deeplyConfirmed = balancesByConfirmation["deeplyConfirmed"] ?: 0L
|
||||
)
|
||||
|
||||
call.respond(response)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, "Error fetching Final wallet balance")
|
||||
}
|
||||
}
|
||||
get("/swapinwalletbalance") {
|
||||
try {
|
||||
val currentBlockHeight: Long = peer.currentTipFlow.filterNotNull().first().toLong()
|
||||
val walletStateFlow = peer.swapInWallet.wallet.walletStateFlow
|
||||
val utxosFlow = walletStateFlow.map { walletState ->
|
||||
walletState.utxos.groupBy { utxo ->
|
||||
val confirmations = currentBlockHeight - utxo.blockHeight + 1
|
||||
when {
|
||||
confirmations < 1 -> "unconfirmed"
|
||||
confirmations < 3 -> "weaklyConfirmed"
|
||||
else -> "deeplyConfirmed"
|
||||
}
|
||||
}.mapValues { entry ->
|
||||
entry.value.sumOf { it.amount.toLong() }
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
val balancesByConfirmation = utxosFlow.first()
|
||||
val response = WalletBalance(
|
||||
unconfirmed = balancesByConfirmation["unconfirmed"] ?: 0L,
|
||||
weaklyConfirmed = balancesByConfirmation["weaklyConfirmed"] ?: 0L,
|
||||
deeplyConfirmed = balancesByConfirmation["deeplyConfirmed"] ?: 0L
|
||||
)
|
||||
call.respond(response)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, "Error fetching Swapin wallet balance")
|
||||
}
|
||||
}
|
||||
get("/swapintransactions") {
|
||||
val wallet = peer.swapInWallet.wallet
|
||||
val walletState = wallet.walletStateFlow.value
|
||||
call.respond(walletState.utxos.toString()) //no serializable json structure for this
|
||||
}
|
||||
post("sendtoaddress") {
|
||||
val res = kotlin.runCatching {
|
||||
val formParameters = call.receiveParameters()
|
||||
@ -231,6 +326,44 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
||||
is Either.Left -> call.respondText(res.value.message.toString())
|
||||
}
|
||||
}
|
||||
post("/splicein") {//Manual splice-in
|
||||
val formParameters = call.receiveParameters()
|
||||
val amountSat = formParameters.getLong("amountSat").msat //the splice in command will send all the balance in wallet
|
||||
val feerate = FeeratePerKw(FeeratePerByte(formParameters.getLong("feerateSatByte").sat))
|
||||
val walletInputs = peer.swapInWallet.wallet.walletStateFlow.value.utxos
|
||||
|
||||
val suitableChannel = peer.channels.values
|
||||
.filterIsInstance<Normal>()
|
||||
.firstOrNull { it.commitments.availableBalanceForReceive() > amountSat }
|
||||
?: return@post call.respond(HttpStatusCode.BadRequest, "No suitable channel available for splice-in")
|
||||
|
||||
if (walletInputs.isEmpty()) {
|
||||
return@post call.respond(HttpStatusCode.BadRequest, "No wallet inputs available for splice-in,swap-in wallet balance too low")
|
||||
}
|
||||
|
||||
try {
|
||||
val spliceCommand = ChannelCommand.Commitment.Splice.Request(
|
||||
replyTo = CompletableDeferred(),
|
||||
spliceIn = walletInputs.let { it1 ->
|
||||
ChannelCommand.Commitment.Splice.Request.SpliceIn(
|
||||
it1, amountSat)
|
||||
},
|
||||
spliceOut = null,
|
||||
requestRemoteFunding = null,
|
||||
feerate = feerate
|
||||
)
|
||||
|
||||
peer.send(WrappedChannelCommand(suitableChannel.channelId, spliceCommand))
|
||||
|
||||
when (val response = spliceCommand.replyTo.await()) {
|
||||
is ChannelCommand.Commitment.Splice.Response.Created -> call.respondText("Splice-in successful: transaction ID ${response.fundingTxId}", status = HttpStatusCode.OK)
|
||||
is ChannelCommand.Commitment.Splice.Response.Failure -> call.respondText("Splice-in failed: $response", status = HttpStatusCode.BadRequest)
|
||||
else -> call.respondText("Splice-in failed: unexpected response type", status = HttpStatusCode.InternalServerError)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, "Failed to process splice-in: ${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
post("closechannel") {
|
||||
val formParameters = call.receiveParameters()
|
||||
val channelId = formParameters.getByteVector32("channelId")
|
||||
|
@ -37,6 +37,8 @@ import fr.acinq.lightning.bin.json.ApiType
|
||||
import fr.acinq.lightning.bin.logs.FileLogWriter
|
||||
import fr.acinq.lightning.bin.logs.TimestampFormatter
|
||||
import fr.acinq.lightning.bin.logs.stringTimestamp
|
||||
import fr.acinq.lightning.blockchain.electrum.ElectrumClient
|
||||
import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher
|
||||
import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient
|
||||
import fr.acinq.lightning.blockchain.mempool.MempoolSpaceWatcher
|
||||
import fr.acinq.lightning.crypto.LocalKeyManager
|
||||
@ -48,6 +50,7 @@ import fr.acinq.lightning.io.TcpSocket
|
||||
import fr.acinq.lightning.logging.LoggerFactory
|
||||
import fr.acinq.lightning.payment.LiquidityPolicy
|
||||
import fr.acinq.lightning.utils.Connection
|
||||
import fr.acinq.lightning.utils.ServerAddress
|
||||
import fr.acinq.lightning.utils.msat
|
||||
import fr.acinq.lightning.utils.sat
|
||||
import fr.acinq.lightning.utils.toByteVector
|
||||
@ -270,9 +273,26 @@ class Phoenixd : CliktCommand() {
|
||||
val paymentsDb = SqlitePaymentsDb(database)
|
||||
|
||||
val mempoolSpace = MempoolSpaceClient(mempoolSpaceUrl, loggerFactory)
|
||||
val watcher = MempoolSpaceWatcher(mempoolSpace, scope, loggerFactory, pollingInterval = mempoolPollingInterval)
|
||||
//val watcher = MempoolSpaceWatcher(mempoolSpace, scope, loggerFactory, pollingInterval = mempoolPollingInterval)
|
||||
|
||||
val electrumClient = ElectrumClient(scope, nodeParams.loggerFactory)
|
||||
val serverAddress = ServerAddress("electrum.acinq.co", 50002, TcpSocket.TLS.UNSAFE_CERTIFICATES)
|
||||
val socketBuilder = TcpSocket.Builder()
|
||||
|
||||
runBlocking {
|
||||
val connected = electrumClient.connect(serverAddress, socketBuilder)
|
||||
if (!connected) {
|
||||
consoleLog(yellow("Failed to connect to Electrum server"))
|
||||
return@runBlocking
|
||||
}
|
||||
else{
|
||||
consoleLog(yellow("Successfully Connected to Electrum Server"))
|
||||
}
|
||||
}
|
||||
|
||||
val electrumWatcher = ElectrumWatcher(electrumClient, scope, nodeParams.loggerFactory)
|
||||
val peer = Peer(
|
||||
nodeParams = nodeParams, walletParams = lsp.walletParams, client = mempoolSpace, watcher = watcher, db = object : Databases {
|
||||
nodeParams = nodeParams, walletParams = lsp.walletParams, client = mempoolSpace, watcher = electrumWatcher, db = object : Databases {
|
||||
override val channels: ChannelsDb get() = channelsDb
|
||||
override val payments: PaymentsDb get() = paymentsDb
|
||||
}, socketBuilder = TcpSocket.Builder(), scope
|
||||
@ -302,7 +322,10 @@ class Phoenixd : CliktCommand() {
|
||||
peer.connectionState.dropWhile { it is Connection.CLOSED }.collect {
|
||||
when (it) {
|
||||
Connection.ESTABLISHING -> consoleLog(yellow("connecting to lightning peer..."))
|
||||
Connection.ESTABLISHED -> consoleLog(yellow("connected to lightning peer"))
|
||||
Connection.ESTABLISHED -> {
|
||||
consoleLog(yellow("connected to lightning peer"))
|
||||
peer.startWatchSwapInWallet()
|
||||
}
|
||||
is Connection.CLOSED -> consoleLog(yellow("disconnected from lightning peer"))
|
||||
}
|
||||
}
|
||||
|
@ -66,8 +66,17 @@ sealed class ApiType {
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Balance(@SerialName("balanceSat") val amount: Satoshi, @SerialName("feeCreditSat") val feeCredit: Satoshi) : ApiType()
|
||||
data class Balance(@SerialName("balanceSat") val amount: Satoshi, @SerialName("feeCreditSat") val feeCredit: Satoshi, @SerialName("swapInSat") val swapInBalance: Satoshi?) : ApiType()
|
||||
|
||||
@Serializable
|
||||
data class SwapInAddress(@SerialName("address") val address: String, @SerialName("index") val index: Int) : ApiType()
|
||||
|
||||
@Serializable
|
||||
data class WalletBalance(
|
||||
val unconfirmed: Long,
|
||||
val weaklyConfirmed: Long,
|
||||
val deeplyConfirmed: Long
|
||||
) : ApiType()
|
||||
@Serializable
|
||||
data class GeneratedInvoice(@SerialName("amountSat") val amount: Satoshi?, val paymentHash: ByteVector32, val serialized: String) : ApiType()
|
||||
|
||||
|
@ -48,6 +48,7 @@ fun main(args: Array<String>) =
|
||||
ListOutgoingPayments(),
|
||||
GetIncomingPayment(),
|
||||
ListIncomingPayments(),
|
||||
DeleteIncomingPayment(),
|
||||
CreateInvoice(),
|
||||
GetOffer(),
|
||||
PayInvoice(),
|
||||
@ -55,7 +56,13 @@ fun main(args: Array<String>) =
|
||||
DecodeInvoice(),
|
||||
DecodeOffer(),
|
||||
SendToAddress(),
|
||||
CloseChannel()
|
||||
CloseChannel(),
|
||||
GetFinalAddress(),
|
||||
GetSwapInAddress(),
|
||||
GetFinalWalletBalance(),
|
||||
GetSwapInWalletBalance(),
|
||||
GetSwapInTransactions(),
|
||||
ManualSpliceIn()
|
||||
)
|
||||
.main(args)
|
||||
|
||||
@ -190,6 +197,13 @@ class ListIncomingPayments : PhoenixCliCommand(name = "listincomingpayments", he
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteIncomingPayment : PhoenixCliCommand(name = "deleteincomingpayment", help = "Delete an incoming payment") {
|
||||
private val paymentHash by option("--paymentHash", "--h").convert { ByteVector32.fromValidHex(it) }.required()
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.delete(url = commonOptions.baseUrl / "payments/incoming/$paymentHash")
|
||||
}
|
||||
}
|
||||
|
||||
class CreateInvoice : PhoenixCliCommand(name = "createinvoice", help = "Create a Lightning invoice", printHelpOnEmptyArgs = true) {
|
||||
private val amountSat by option("--amountSat").long()
|
||||
private val description by mutuallyExclusiveOptions(
|
||||
@ -273,6 +287,36 @@ class DecodeOffer : PhoenixCliCommand(name = "decodeoffer", help = "Decode a Lig
|
||||
}
|
||||
}
|
||||
|
||||
class GetFinalAddress : PhoenixCliCommand(name = "getfinaladdress", help = "Retrieve the final wallet address", printHelpOnEmptyArgs = true) {
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.get(url = commonOptions.baseUrl / "getfinaladdress")
|
||||
}
|
||||
}
|
||||
|
||||
class GetSwapInAddress : PhoenixCliCommand(name = "getswapinaddress", help = "Retrieve the current swap-in address from the wallet", printHelpOnEmptyArgs = true) {
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.get(url = commonOptions.baseUrl / "getswapinaddress")
|
||||
}
|
||||
}
|
||||
|
||||
class GetFinalWalletBalance : PhoenixCliCommand(name = "getfinalwalletbalance", help = "Retrieve the final wallet balance", printHelpOnEmptyArgs = true) {
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.get(url = commonOptions.baseUrl / "finalwalletbalance")
|
||||
}
|
||||
}
|
||||
|
||||
class GetSwapInWalletBalance : PhoenixCliCommand(name = "getswapinwalletbalance", help = "Retrieve the swap-in wallet balance", printHelpOnEmptyArgs = true) {
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.get(url = commonOptions.baseUrl / "swapinwalletbalance")
|
||||
}
|
||||
}
|
||||
|
||||
class GetSwapInTransactions : PhoenixCliCommand(name = "getswapintransactions", help = "List transactions for the swap-in wallet", printHelpOnEmptyArgs = true) {
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.get(url = commonOptions.baseUrl / "swapintransactions")
|
||||
}
|
||||
}
|
||||
|
||||
class SendToAddress : PhoenixCliCommand(name = "sendtoaddress", help = "Send to a Bitcoin address", printHelpOnEmptyArgs = true) {
|
||||
private val amountSat by option("--amountSat").long().required()
|
||||
private val address by option("--address").required().check { runCatching { Base58Check.decode(it) }.isSuccess || runCatching { Bech32.decodeWitnessAddress(it) }.isSuccess }
|
||||
@ -289,6 +333,21 @@ class SendToAddress : PhoenixCliCommand(name = "sendtoaddress", help = "Send to
|
||||
}
|
||||
}
|
||||
|
||||
class ManualSpliceIn : PhoenixCliCommand(name = "splicein", help = "Splice in funds to a channel using all available balance in the wallet", printHelpOnEmptyArgs = true) {
|
||||
private val amountSat by option("--amountSat").long().required() //not necessarily required, come back to it
|
||||
private val feerateSatByte by option("--feerateSatByte").int().required()
|
||||
|
||||
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||
it.submitForm(
|
||||
url = (commonOptions.baseUrl / "splicein").toString(),
|
||||
formParameters = parameters {
|
||||
append("amountSat", amountSat.toString())
|
||||
append("feerateSatByte", feerateSatByte.toString())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CloseChannel : PhoenixCliCommand(name = "closechannel", help = "Close channel", printHelpOnEmptyArgs = true) {
|
||||
private val channelId by option("--channelId").convert { it.toByteVector32() }.required()
|
||||
private val address by option("--address").required().check { runCatching { Base58Check.decode(it) }.isSuccess || runCatching { Bech32.decodeWitnessAddress(it) }.isSuccess }
|
||||
|
Loading…
x
Reference in New Issue
Block a user