Add BOLT12 support (#60)
BOLT12 introduces "offers", which are reusable payment requests (a.k.a. static invoices). Add the following phoenix-cli/api methods: - `getoffer` - `payoffer` - `decodeinvoice` (supersedes #56) - `decodeoffer` --------- Co-authored-by: Dominique Padiou <5765435+dpad85@users.noreply.github.com>
This commit is contained in:
parent
4a7e2a4921
commit
18ade318cd
@ -8,6 +8,7 @@ import fr.acinq.bitcoin.utils.Either
|
|||||||
import fr.acinq.bitcoin.utils.toEither
|
import fr.acinq.bitcoin.utils.toEither
|
||||||
import fr.acinq.lightning.BuildVersions
|
import fr.acinq.lightning.BuildVersions
|
||||||
import fr.acinq.lightning.Lightning.randomBytes32
|
import fr.acinq.lightning.Lightning.randomBytes32
|
||||||
|
import fr.acinq.lightning.Lightning.randomKey
|
||||||
import fr.acinq.lightning.NodeParams
|
import fr.acinq.lightning.NodeParams
|
||||||
import fr.acinq.lightning.bin.db.SqlitePaymentsDb
|
import fr.acinq.lightning.bin.db.SqlitePaymentsDb
|
||||||
import fr.acinq.lightning.bin.db.WalletPaymentId
|
import fr.acinq.lightning.bin.db.WalletPaymentId
|
||||||
@ -25,6 +26,7 @@ import fr.acinq.lightning.io.Peer
|
|||||||
import fr.acinq.lightning.io.WrappedChannelCommand
|
import fr.acinq.lightning.io.WrappedChannelCommand
|
||||||
import fr.acinq.lightning.payment.Bolt11Invoice
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
import fr.acinq.lightning.utils.*
|
import fr.acinq.lightning.utils.*
|
||||||
|
import fr.acinq.lightning.wire.OfferTypes
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
@ -43,16 +45,20 @@ import io.ktor.server.routing.*
|
|||||||
import io.ktor.server.websocket.*
|
import io.ktor.server.websocket.*
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okio.ByteString.Companion.encodeUtf8
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class Api(private val nodeParams: NodeParams, private val peer: Peer, private val eventsFlow: SharedFlow<ApiEvent>, private val password: String, private val webhookUrl: Url?, private val webhookSecret: String) {
|
class Api(private val nodeParams: NodeParams, private val peer: Peer, private val eventsFlow: SharedFlow<ApiEvent>, private val password: String, private val webhookUrl: Url?, private val webhookSecret: String) {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
|
|
||||||
val json = Json {
|
val json = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
isLenient = true
|
isLenient = true
|
||||||
|
explicitNulls = false
|
||||||
serializersModule = fr.acinq.lightning.json.JsonSerializers.json.serializersModule
|
serializersModule = fr.acinq.lightning.json.JsonSerializers.json.serializersModule
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +137,9 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
|||||||
}
|
}
|
||||||
call.respond(GeneratedInvoice(invoice.amount?.truncateToSatoshi(), invoice.paymentHash, serialized = invoice.write()))
|
call.respond(GeneratedInvoice(invoice.amount?.truncateToSatoshi(), invoice.paymentHash, serialized = invoice.write()))
|
||||||
}
|
}
|
||||||
|
get("getoffer") {
|
||||||
|
call.respond(nodeParams.defaultOffer(peer.walletParams.trampolineNode.id).encode())
|
||||||
|
}
|
||||||
get("payments/incoming") {
|
get("payments/incoming") {
|
||||||
val listAll = call.parameters["all"]?.toBoolean() ?: false // by default, only list incoming payments that have been received
|
val listAll = call.parameters["all"]?.toBoolean() ?: false // by default, only list incoming payments that have been received
|
||||||
val externalId = call.parameters["externalId"] // may filter incoming payments by an external id
|
val externalId = call.parameters["externalId"] // may filter incoming payments by an external id
|
||||||
@ -180,9 +189,30 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
|||||||
when (val event = peer.payInvoice(amount, invoice)) {
|
when (val event = peer.payInvoice(amount, invoice)) {
|
||||||
is fr.acinq.lightning.io.PaymentSent -> call.respond(PaymentSent(event))
|
is fr.acinq.lightning.io.PaymentSent -> call.respond(PaymentSent(event))
|
||||||
is fr.acinq.lightning.io.PaymentNotSent -> call.respond(PaymentFailed(event))
|
is fr.acinq.lightning.io.PaymentNotSent -> call.respond(PaymentFailed(event))
|
||||||
is fr.acinq.lightning.io.OfferNotPaid -> TODO()
|
is fr.acinq.lightning.io.OfferNotPaid -> error("unreachable code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
post("payoffer") {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val overrideAmount = formParameters["amountSat"]?.let { it.toLongOrNull() ?: invalidType("amountSat", "integer") }?.sat?.toMilliSatoshi()
|
||||||
|
val offer = formParameters.getOffer("offer")
|
||||||
|
val amount = (overrideAmount ?: offer.amount) ?: missing("amountSat")
|
||||||
|
when (val event = peer.payOffer(amount, offer, randomKey(), fetchInvoiceTimeout = 30.seconds)) {
|
||||||
|
is fr.acinq.lightning.io.PaymentSent -> call.respond(PaymentSent(event))
|
||||||
|
is fr.acinq.lightning.io.PaymentNotSent -> call.respond(PaymentFailed(event))
|
||||||
|
is fr.acinq.lightning.io.OfferNotPaid -> call.respond(PaymentFailed(event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post("decodeinvoice") {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val invoice = formParameters.getInvoice("invoice")
|
||||||
|
call.respond(invoice)
|
||||||
|
}
|
||||||
|
post("decodeoffer") {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val offer = formParameters.getOffer("offer")
|
||||||
|
call.respond(offer)
|
||||||
|
}
|
||||||
post("sendtoaddress") {
|
post("sendtoaddress") {
|
||||||
val res = kotlin.runCatching {
|
val res = kotlin.runCatching {
|
||||||
val formParameters = call.receiveParameters()
|
val formParameters = call.receiveParameters()
|
||||||
@ -268,6 +298,8 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
|
|||||||
|
|
||||||
private fun Parameters.getInvoice(argName: String): Bolt11Invoice = getString(argName).let { invoice -> Bolt11Invoice.read(invoice).getOrElse { invalidType(argName, "bolt11invoice") } }
|
private fun Parameters.getInvoice(argName: String): Bolt11Invoice = getString(argName).let { invoice -> Bolt11Invoice.read(invoice).getOrElse { invalidType(argName, "bolt11invoice") } }
|
||||||
|
|
||||||
|
private fun Parameters.getOffer(argName: String): OfferTypes.Offer = getString(argName).let { invoice -> OfferTypes.Offer.decode(invoice).getOrElse { invalidType(argName, "offer") } }
|
||||||
|
|
||||||
private fun Parameters.getLong(argName: String): Long = ((this[argName] ?: missing(argName)).toLongOrNull()) ?: invalidType(argName, "integer")
|
private fun Parameters.getLong(argName: String): Long = ((this[argName] ?: missing(argName)).toLongOrNull()) ?: invalidType(argName, "integer")
|
||||||
|
|
||||||
private fun Parameters.getOptionalLong(argName: String): Long? = this[argName]?.let { it.toLongOrNull() ?: invalidType(argName, "integer") }
|
private fun Parameters.getOptionalLong(argName: String): Long? = this[argName]?.let { it.toLongOrNull() ?: invalidType(argName, "integer") }
|
||||||
|
@ -242,6 +242,7 @@ class Phoenixd : CliktCommand() {
|
|||||||
liquidityPolicy = MutableStateFlow(liquidityPolicy),
|
liquidityPolicy = MutableStateFlow(liquidityPolicy),
|
||||||
)
|
)
|
||||||
consoleLog(cyan("nodeid: ${nodeParams.nodeId}"))
|
consoleLog(cyan("nodeid: ${nodeParams.nodeId}"))
|
||||||
|
consoleLog(cyan("offer: ${nodeParams.defaultOffer(lsp.walletParams.trampolineNode.id)}"))
|
||||||
|
|
||||||
val driver = createAppDbDriver(datadir, chain, nodeParams.nodeId)
|
val driver = createAppDbDriver(datadir, chain, nodeParams.nodeId)
|
||||||
val database = PhoenixDatabase(
|
val database = PhoenixDatabase(
|
||||||
|
@ -17,10 +17,12 @@
|
|||||||
@file:UseSerializers(
|
@file:UseSerializers(
|
||||||
OutpointSerializer::class,
|
OutpointSerializer::class,
|
||||||
ByteVector32Serializer::class,
|
ByteVector32Serializer::class,
|
||||||
|
ByteVectorSerializer::class,
|
||||||
)
|
)
|
||||||
|
|
||||||
package fr.acinq.lightning.bin.db.payments
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector
|
||||||
import fr.acinq.bitcoin.ByteVector32
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
import fr.acinq.bitcoin.OutPoint
|
import fr.acinq.bitcoin.OutPoint
|
||||||
import fr.acinq.bitcoin.TxId
|
import fr.acinq.bitcoin.TxId
|
||||||
@ -28,7 +30,9 @@ import fr.acinq.lightning.bin.db.payments.DbTypesHelper.decodeBlob
|
|||||||
import fr.acinq.lightning.db.IncomingPayment
|
import fr.acinq.lightning.db.IncomingPayment
|
||||||
import fr.acinq.lightning.payment.Bolt11Invoice
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVectorSerializer
|
||||||
import fr.acinq.lightning.bin.db.serializers.v1.OutpointSerializer
|
import fr.acinq.lightning.bin.db.serializers.v1.OutpointSerializer
|
||||||
|
import fr.acinq.lightning.payment.OfferPaymentMetadata
|
||||||
import io.ktor.utils.io.charsets.*
|
import io.ktor.utils.io.charsets.*
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.*
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
@ -39,6 +43,7 @@ enum class IncomingOriginTypeVersion {
|
|||||||
INVOICE_V0,
|
INVOICE_V0,
|
||||||
SWAPIN_V0,
|
SWAPIN_V0,
|
||||||
ONCHAIN_V0,
|
ONCHAIN_V0,
|
||||||
|
OFFER_V0,
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class IncomingOriginData {
|
sealed class IncomingOriginData {
|
||||||
@ -58,12 +63,21 @@ sealed class IncomingOriginData {
|
|||||||
data class V0(@Serializable val txId: ByteVector32, val outpoints: List<@Serializable OutPoint>) : SwapIn()
|
data class V0(@Serializable val txId: ByteVector32, val outpoints: List<@Serializable OutPoint>) : SwapIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class Offer : IncomingOriginData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(@Serializable val encodedMetadata: ByteVector) : Offer()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun deserialize(typeVersion: IncomingOriginTypeVersion, blob: ByteArray): IncomingPayment.Origin = decodeBlob(blob) { json, format ->
|
fun deserialize(typeVersion: IncomingOriginTypeVersion, blob: ByteArray): IncomingPayment.Origin = decodeBlob(blob) { json, format ->
|
||||||
when (typeVersion) {
|
when (typeVersion) {
|
||||||
IncomingOriginTypeVersion.INVOICE_V0 -> format.decodeFromString<Invoice.V0>(json).let { IncomingPayment.Origin.Invoice(Bolt11Invoice.read(it.paymentRequest).get()) }
|
IncomingOriginTypeVersion.INVOICE_V0 -> format.decodeFromString<Invoice.V0>(json).let { IncomingPayment.Origin.Invoice(Bolt11Invoice.read(it.paymentRequest).get()) }
|
||||||
IncomingOriginTypeVersion.SWAPIN_V0 -> format.decodeFromString<SwapIn.V0>(json).let { IncomingPayment.Origin.SwapIn(it.address) }
|
IncomingOriginTypeVersion.SWAPIN_V0 -> format.decodeFromString<SwapIn.V0>(json).let { IncomingPayment.Origin.SwapIn(it.address) }
|
||||||
IncomingOriginTypeVersion.ONCHAIN_V0 -> format.decodeFromString<OnChain.V0>(json).let { IncomingPayment.Origin.OnChain(TxId(it.txId), it.outpoints.toSet()) }
|
IncomingOriginTypeVersion.ONCHAIN_V0 -> format.decodeFromString<OnChain.V0>(json).let { IncomingPayment.Origin.OnChain(TxId(it.txId), it.outpoints.toSet()) }
|
||||||
|
IncomingOriginTypeVersion.OFFER_V0 -> format.decodeFromString<Offer.V0>(json).let {
|
||||||
|
IncomingPayment.Origin.Offer(metadata = OfferPaymentMetadata.decode(it.encodedMetadata))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,5 +90,6 @@ fun IncomingPayment.Origin.mapToDb(): Pair<IncomingOriginTypeVersion, ByteArray>
|
|||||||
Json.encodeToString(IncomingOriginData.SwapIn.V0(address)).toByteArray(Charsets.UTF_8)
|
Json.encodeToString(IncomingOriginData.SwapIn.V0(address)).toByteArray(Charsets.UTF_8)
|
||||||
is IncomingPayment.Origin.OnChain -> IncomingOriginTypeVersion.ONCHAIN_V0 to
|
is IncomingPayment.Origin.OnChain -> IncomingOriginTypeVersion.ONCHAIN_V0 to
|
||||||
Json.encodeToString(IncomingOriginData.OnChain.V0(txId.value, localInputs.toList())).toByteArray(Charsets.UTF_8)
|
Json.encodeToString(IncomingOriginData.OnChain.V0(txId.value, localInputs.toList())).toByteArray(Charsets.UTF_8)
|
||||||
is IncomingPayment.Origin.Offer -> TODO()
|
is IncomingPayment.Origin.Offer -> IncomingOriginTypeVersion.OFFER_V0 to
|
||||||
|
Json.encodeToString(IncomingOriginData.Offer.V0(metadata.encode())).toByteArray(Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,13 @@
|
|||||||
|
|
||||||
package fr.acinq.lightning.bin.db.payments
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
import fr.acinq.bitcoin.ByteVector32
|
import fr.acinq.bitcoin.PrivateKey
|
||||||
import fr.acinq.bitcoin.Satoshi
|
import fr.acinq.bitcoin.Satoshi
|
||||||
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer
|
import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer
|
||||||
import fr.acinq.lightning.db.LightningOutgoingPayment
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
import fr.acinq.lightning.payment.Bolt11Invoice
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
|
import fr.acinq.lightning.payment.Bolt12Invoice
|
||||||
import io.ktor.utils.io.charsets.*
|
import io.ktor.utils.io.charsets.*
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -38,6 +39,7 @@ import kotlinx.serialization.json.Json
|
|||||||
enum class LightningOutgoingDetailsTypeVersion {
|
enum class LightningOutgoingDetailsTypeVersion {
|
||||||
NORMAL_V0,
|
NORMAL_V0,
|
||||||
SWAPOUT_V0,
|
SWAPOUT_V0,
|
||||||
|
BLINDED_V0
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class LightningOutgoingDetailsData {
|
sealed class LightningOutgoingDetailsData {
|
||||||
@ -52,12 +54,33 @@ sealed class LightningOutgoingDetailsData {
|
|||||||
data class V0(val address: String, val paymentRequest: String, @Serializable val swapOutFee: Satoshi) : SwapOut()
|
data class V0(val address: String, val paymentRequest: String, @Serializable val swapOutFee: Satoshi) : SwapOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class Blinded : LightningOutgoingDetailsData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val paymentRequest: String, val payerKey: String) : Blinded()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Deserialize the details of an outgoing payment. Return null if the details is for a legacy channel closing payment (see [deserializeLegacyClosingDetails]). */
|
/** Deserialize the details of an outgoing payment. Return null if the details is for a legacy channel closing payment (see [deserializeLegacyClosingDetails]). */
|
||||||
fun deserialize(typeVersion: LightningOutgoingDetailsTypeVersion, blob: ByteArray): LightningOutgoingPayment.Details = DbTypesHelper.decodeBlob(blob) { json, format ->
|
fun deserialize(typeVersion: LightningOutgoingDetailsTypeVersion, blob: ByteArray): LightningOutgoingPayment.Details = DbTypesHelper.decodeBlob(blob) { json, format ->
|
||||||
when (typeVersion) {
|
when (typeVersion) {
|
||||||
LightningOutgoingDetailsTypeVersion.NORMAL_V0 -> format.decodeFromString<Normal.V0>(json).let { LightningOutgoingPayment.Details.Normal(Bolt11Invoice.read(it.paymentRequest).get()) }
|
LightningOutgoingDetailsTypeVersion.NORMAL_V0 -> format.decodeFromString<Normal.V0>(json).let {
|
||||||
LightningOutgoingDetailsTypeVersion.SWAPOUT_V0 -> format.decodeFromString<SwapOut.V0>(json).let { LightningOutgoingPayment.Details.SwapOut(it.address, Bolt11Invoice.read(it.paymentRequest).get(), it.swapOutFee) }
|
LightningOutgoingPayment.Details.Normal(
|
||||||
|
paymentRequest = Bolt11Invoice.read(it.paymentRequest).get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LightningOutgoingDetailsTypeVersion.SWAPOUT_V0 -> format.decodeFromString<SwapOut.V0>(json).let {
|
||||||
|
LightningOutgoingPayment.Details.SwapOut(
|
||||||
|
address = it.address,
|
||||||
|
paymentRequest = Bolt11Invoice.read(it.paymentRequest).get(),
|
||||||
|
swapOutFee = it.swapOutFee
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LightningOutgoingDetailsTypeVersion.BLINDED_V0 -> format.decodeFromString<Blinded.V0>(json).let {
|
||||||
|
LightningOutgoingPayment.Details.Blinded(
|
||||||
|
paymentRequest = Bolt12Invoice.fromString(it.paymentRequest).get(),
|
||||||
|
payerKey = PrivateKey.fromHex(it.payerKey),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,5 +91,6 @@ fun LightningOutgoingPayment.Details.mapToDb(): Pair<LightningOutgoingDetailsTyp
|
|||||||
Json.encodeToString(LightningOutgoingDetailsData.Normal.V0(paymentRequest.write())).toByteArray(Charsets.UTF_8)
|
Json.encodeToString(LightningOutgoingDetailsData.Normal.V0(paymentRequest.write())).toByteArray(Charsets.UTF_8)
|
||||||
is LightningOutgoingPayment.Details.SwapOut -> LightningOutgoingDetailsTypeVersion.SWAPOUT_V0 to
|
is LightningOutgoingPayment.Details.SwapOut -> LightningOutgoingDetailsTypeVersion.SWAPOUT_V0 to
|
||||||
Json.encodeToString(LightningOutgoingDetailsData.SwapOut.V0(address, paymentRequest.write(), swapOutFee)).toByteArray(Charsets.UTF_8)
|
Json.encodeToString(LightningOutgoingDetailsData.SwapOut.V0(address, paymentRequest.write(), swapOutFee)).toByteArray(Charsets.UTF_8)
|
||||||
is LightningOutgoingPayment.Details.Blinded -> TODO()
|
is LightningOutgoingPayment.Details.Blinded -> LightningOutgoingDetailsTypeVersion.BLINDED_V0 to
|
||||||
|
Json.encodeToString(LightningOutgoingDetailsData.Blinded.V0(paymentRequest.write(), payerKey.toHex())).toByteArray(Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
|
@ -96,8 +96,9 @@ sealed class ApiType {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("payment_failed")
|
@SerialName("payment_failed")
|
||||||
data class PaymentFailed(val paymentHash: ByteVector32, val reason: String) : ApiType() {
|
data class PaymentFailed(val paymentHash: ByteVector32?, val offerId: ByteVector32?, val reason: String) : ApiType() {
|
||||||
constructor(event: fr.acinq.lightning.io.PaymentNotSent) : this(event.request.paymentHash, event.reason.explain().fold({ it.toString() }, { it.toString() }))
|
constructor(event: fr.acinq.lightning.io.PaymentNotSent) : this(paymentHash = event.request.paymentHash, offerId = null, reason = event.reason.explain().fold({ it.toString() }, { it.toString() }))
|
||||||
|
constructor(event: fr.acinq.lightning.io.OfferNotPaid) : this(paymentHash = null, offerId = event.request.offer.offerId, event.reason.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -22,6 +22,7 @@ import fr.acinq.lightning.bin.conf.readConfFile
|
|||||||
import fr.acinq.lightning.bin.datadir
|
import fr.acinq.lightning.bin.datadir
|
||||||
import fr.acinq.lightning.payment.Bolt11Invoice
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
import fr.acinq.lightning.utils.UUID
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import fr.acinq.lightning.wire.OfferTypes
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.plugins.auth.*
|
import io.ktor.client.plugins.auth.*
|
||||||
import io.ktor.client.plugins.auth.providers.*
|
import io.ktor.client.plugins.auth.providers.*
|
||||||
@ -39,7 +40,23 @@ import kotlinx.serialization.json.Json
|
|||||||
fun main(args: Array<String>) =
|
fun main(args: Array<String>) =
|
||||||
PhoenixCli()
|
PhoenixCli()
|
||||||
.versionOption(BuildVersions.phoenixdVersion, names = setOf("--version", "-v"))
|
.versionOption(BuildVersions.phoenixdVersion, names = setOf("--version", "-v"))
|
||||||
.subcommands(GetInfo(), GetBalance(), ListChannels(), GetOutgoingPayment(), ListOutgoingPayments(), GetIncomingPayment(), ListIncomingPayments(), CreateInvoice(), PayInvoice(), SendToAddress(), CloseChannel())
|
.subcommands(
|
||||||
|
GetInfo(),
|
||||||
|
GetBalance(),
|
||||||
|
ListChannels(),
|
||||||
|
GetOutgoingPayment(),
|
||||||
|
ListOutgoingPayments(),
|
||||||
|
GetIncomingPayment(),
|
||||||
|
ListIncomingPayments(),
|
||||||
|
CreateInvoice(),
|
||||||
|
GetOffer(),
|
||||||
|
PayInvoice(),
|
||||||
|
PayOffer(),
|
||||||
|
DecodeInvoice(),
|
||||||
|
DecodeOffer(),
|
||||||
|
SendToAddress(),
|
||||||
|
CloseChannel()
|
||||||
|
)
|
||||||
.main(args)
|
.main(args)
|
||||||
|
|
||||||
data class HttpConf(val baseUrl: Url, val httpClient: HttpClient)
|
data class HttpConf(val baseUrl: Url, val httpClient: HttpClient)
|
||||||
@ -196,6 +213,12 @@ class CreateInvoice : PhoenixCliCommand(name = "createinvoice", help = "Create a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GetOffer : PhoenixCliCommand(name = "getoffer", help = "Return a Lightning offer (static invoice)") {
|
||||||
|
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||||
|
it.get(url = commonOptions.baseUrl / "getoffer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PayInvoice : PhoenixCliCommand(name = "payinvoice", help = "Pay a Lightning invoice", printHelpOnEmptyArgs = true) {
|
class PayInvoice : PhoenixCliCommand(name = "payinvoice", help = "Pay a Lightning invoice", printHelpOnEmptyArgs = true) {
|
||||||
private val amountSat by option("--amountSat").long()
|
private val amountSat by option("--amountSat").long()
|
||||||
private val invoice by option("--invoice").required().check { Bolt11Invoice.read(it).isSuccess }
|
private val invoice by option("--invoice").required().check { Bolt11Invoice.read(it).isSuccess }
|
||||||
@ -210,6 +233,44 @@ class PayInvoice : PhoenixCliCommand(name = "payinvoice", help = "Pay a Lightnin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PayOffer : PhoenixCliCommand(name = "payoffer", help = "Pay a Lightning offer", printHelpOnEmptyArgs = true) {
|
||||||
|
private val amountSat by option("--amountSat").long()
|
||||||
|
private val invoice by option("--offer").required().check { OfferTypes.Offer.decode(it).isSuccess }
|
||||||
|
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||||
|
it.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "payoffer").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
amountSat?.let { append("amountSat", amountSat.toString()) }
|
||||||
|
append("offer", invoice)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DecodeInvoice : PhoenixCliCommand(name = "decodeinvoice", help = "Decode a Lightning invoice", printHelpOnEmptyArgs = true) {
|
||||||
|
private val invoice by option("--invoice").required().check { Bolt11Invoice.read(it).isSuccess }
|
||||||
|
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||||
|
it.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "decodeinvoice").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
append("invoice", invoice)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DecodeOffer : PhoenixCliCommand(name = "decodeoffer", help = "Decode a Lightning offer", printHelpOnEmptyArgs = true) {
|
||||||
|
private val invoice by option("--offer").required().check { OfferTypes.Offer.decode(it).isSuccess }
|
||||||
|
override suspend fun httpRequest() = commonOptions.httpClient.use {
|
||||||
|
it.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "decodeoffer").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
append("offer", invoice)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SendToAddress : PhoenixCliCommand(name = "sendtoaddress", help = "Send to a Bitcoin address", printHelpOnEmptyArgs = true) {
|
class SendToAddress : PhoenixCliCommand(name = "sendtoaddress", help = "Send to a Bitcoin address", printHelpOnEmptyArgs = true) {
|
||||||
private val amountSat by option("--amountSat").long().required()
|
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 }
|
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