Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On-Chain Wallets #69

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
148 changes: 147 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ 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.crypto.KeyManager
import fr.acinq.lightning.crypto.div
import fr.acinq.lightning.crypto.LocalKeyManager
import fr.acinq.lightning.io.Peer
import fr.acinq.lightning.io.WrappedChannelCommand
Expand All @@ -52,7 +55,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
Expand Down Expand Up @@ -135,7 +143,10 @@ class Api(
.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())
Expand Down Expand Up @@ -210,6 +221,15 @@ class Api(
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()
Expand Down Expand Up @@ -272,6 +292,94 @@ class Api(
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
}
get("/getfinalwalletinfo"){
val finalOnChainWallet = nodeParams.keyManager.finalOnChainWallet
val path = (KeyManager.Bip84OnChainKeys.bip84BasePath(nodeParams.chain) / finalOnChainWallet.account).toString()
call.respond(FinalWalletInfo(path, finalOnChainWallet.xpub))
}
get("/getswapinwalletinfo"){
val swapInOnChainWallet = nodeParams.keyManager.swapInOnChainWallet
call.respond(SwapInWalletInfo(swapInOnChainWallet.legacyDescriptor, swapInOnChainWallet.publicDescriptor, swapInOnChainWallet.userPublicKey.toHex()))
}
post("lnurlpay") {
val formParameters = call.receiveParameters()
val overrideAmount = formParameters["amountSat"]?.let { it.toLongOrNull() ?: invalidType("amountSat", "integer") }?.sat?.toMilliSatoshi()
Expand Down Expand Up @@ -354,6 +462,44 @@ class Api(
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")
Expand Down
29 changes: 26 additions & 3 deletions src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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
Expand All @@ -49,6 +51,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
Expand Down Expand Up @@ -276,9 +279,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
Expand Down Expand Up @@ -308,7 +328,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"))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,23 @@ 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 FinalWalletInfo(@SerialName("path") val path: String, @SerialName("xpub") val xpub: String) : ApiType()

@Serializable
data class SwapInWalletInfo(@SerialName("legacyDescriptor") val legacyDescriptor: String, @SerialName("publicDescriptor") val publicDescriptor: String, @SerialName("userPublicKey") val userPublicKey: String) : ApiType()

@Serializable
data class GeneratedInvoice(@SerialName("amountSat") val amount: Satoshi?, val paymentHash: ByteVector32, val serialized: String) : ApiType()
Expand Down
Loading