Skip to content

Commit

Permalink
Add json serializers for invoices and offers (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
pm47 authored Jun 14, 2024
1 parent 4c6fd6b commit f739e46
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 22 deletions.
120 changes: 120 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@
JsonSerializers.CommitSigTlvSerializer::class,
JsonSerializers.UUIDSerializer::class,
JsonSerializers.ClosingSerializer::class,
JsonSerializers.Bolt11ExtraHopSerializer::class,
JsonSerializers.Bolt11UnknownTagSerializer::class,
JsonSerializers.Bolt11InvalidTagSerializer::class,
JsonSerializers.EncodedNodeIdSerializer::class,
JsonSerializers.BlindedNodeSerializer::class,
JsonSerializers.BlindedRouteSerializer::class,
)
@file:UseContextualSerialization(
PersistedChannelState::class
Expand All @@ -107,10 +113,13 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.channel.*
import fr.acinq.lightning.channel.states.*
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.crypto.RouteBlinding
import fr.acinq.lightning.crypto.ShaChain
import fr.acinq.lightning.json.JsonSerializers.LongSerializer
import fr.acinq.lightning.json.JsonSerializers.StringSerializer
import fr.acinq.lightning.json.JsonSerializers.SurrogateSerializer
import fr.acinq.lightning.payment.Bolt11Invoice
import fr.acinq.lightning.payment.Bolt11Invoice.TaggedField
import fr.acinq.lightning.transactions.CommitmentSpec
import fr.acinq.lightning.transactions.IncomingHtlc
import fr.acinq.lightning.transactions.OutgoingHtlc
Expand Down Expand Up @@ -569,4 +578,115 @@ object JsonSerializers {
object WaitingForRevocationSerializer

object UUIDSerializer : StringSerializer<UUID>()

@Serializer(forClass = TaggedField.ExtraHop::class)
object Bolt11ExtraHopSerializer

@Serializer(forClass = TaggedField.UnknownTag::class)
object Bolt11UnknownTagSerializer

@Serializer(forClass = TaggedField.InvalidTag::class)
object Bolt11InvalidTagSerializer

@Serializable
data class Bolt11InvoiceSurrogate(
val chain: String,
val amount: MilliSatoshi?,
val paymentHash: ByteVector32,
val description: String?,
val descriptionHash: ByteVector32?,
val minFinalCltvExpiryDelta: CltvExpiryDelta?,
val paymentSecret: ByteVector32,
val paymentMetadata: ByteVector?,
val expirySeconds: Long?,
val extraHops: List<List<TaggedField.ExtraHop>>,
val features: Features?,
val timestampSeconds: Long,
val unknownTags: List<TaggedField.UnknownTag>?,
val invalidTags: List<TaggedField.InvalidTag>?,
)

object Bolt11InvoiceSerializer : SurrogateSerializer<Bolt11Invoice, Bolt11InvoiceSurrogate>(
transform = { o ->
Bolt11InvoiceSurrogate(
chain = o.chain?.name?.lowercase() ?: "unknown",
amount = o.amount,
paymentHash = o.paymentHash,
description = o.description,
descriptionHash = o.descriptionHash,
minFinalCltvExpiryDelta = o.minFinalExpiryDelta,
paymentSecret = o.paymentSecret,
paymentMetadata = o.paymentMetadata,
expirySeconds = o.expirySeconds,
extraHops = o.routingInfo.map { it.hints },
features = o.features.let { if (it == Features.empty) null else it },
timestampSeconds = o.timestampSeconds,
unknownTags = o.tags.filterIsInstance<TaggedField.UnknownTag>().run { ifEmpty { null } },
invalidTags = o.tags.filterIsInstance<TaggedField.InvalidTag>().run { ifEmpty { null } }
)
},
delegateSerializer = Bolt11InvoiceSurrogate.serializer()
)

@Serializable
data class EncodedNodeIdSurrogate(val isNode1: Boolean?, val scid: ShortChannelId?, val publicKey: PublicKey?, val walletPublicKey: PublicKey?)
object EncodedNodeIdSerializer : SurrogateSerializer<EncodedNodeId, EncodedNodeIdSurrogate>(
transform = { o ->
when (o) {
is EncodedNodeId.WithPublicKey.Plain -> EncodedNodeIdSurrogate(isNode1 = null, scid = null, publicKey = o.publicKey, walletPublicKey = null)
is EncodedNodeId.WithPublicKey.Wallet -> EncodedNodeIdSurrogate(isNode1 = null, scid = null, publicKey = null, walletPublicKey = o.publicKey)
is EncodedNodeId.ShortChannelIdDir -> EncodedNodeIdSurrogate(isNode1 = o.isNode1, scid = o.scid, publicKey = null, walletPublicKey = null)
}
},
delegateSerializer = EncodedNodeIdSurrogate.serializer()
)

@Serializer(forClass = RouteBlinding.BlindedNode::class)
object BlindedNodeSerializer

@Serializer(forClass = RouteBlinding.BlindedRoute::class)
object BlindedRouteSerializer

@Serializable
data class OfferSurrogate(
val chain: String,
val chainHashes: List<BlockHash>?,
val amount: MilliSatoshi?,
val currency: String?,
val issuer: String?,
val quantityMax: Long?,
val description: String?,
val metadata: ByteVector?,
val expirySeconds: Long?,
val nodeId: PublicKey?,
val path: List<RouteBlinding.BlindedRoute>?,
val features: Features?,
val unknownTlvs: List<GenericTlv>?
)
object OfferSerializer : SurrogateSerializer<OfferTypes.Offer, OfferSurrogate>(
transform = { o ->
OfferSurrogate(
chain = when {
o.chains.isEmpty() -> Chain.Mainnet.name
o.chains.contains(Chain.Mainnet.chainHash) && o.chains.size == 1 -> Chain.Mainnet.name
o.chains.contains(Chain.Testnet.chainHash) && o.chains.size == 1 -> Chain.Testnet.name
o.chains.contains(Chain.Regtest.chainHash) && o.chains.size == 1 -> Chain.Regtest.name
else -> "unknown"
}.lowercase(),
chainHashes = o.chains.run { ifEmpty { null } },
amount = o.amount,
currency = o.currency,
issuer = o.issuer,
quantityMax = o.quantityMax,
description = o.description,
metadata = o.metadata,
expirySeconds = o.expirySeconds,
nodeId = o.nodeId,
path = o.paths?.map { it.route }?.run { ifEmpty { null } },
features = o.features.let { if (it == Features.empty) null else it },
unknownTlvs = o.records.unknown.toList().run { ifEmpty { null } }
)
},
delegateSerializer = OfferSurrogate.serializer()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ data class Bolt11Invoice(
val tags: List<TaggedField>,
val signature: ByteVector
) : PaymentRequest() {
val chain: Chain? = prefixes.entries.firstOrNull { it.value == prefix }?.key

override val paymentHash: ByteVector32 = tags.find { it is TaggedField.PaymentHash }!!.run { (this as TaggedField.PaymentHash).hash }

val paymentSecret: ByteVector32 = tags.find { it is TaggedField.PaymentSecret }!!.run { (this as TaggedField.PaymentSecret).secret }
Expand Down Expand Up @@ -105,13 +107,13 @@ data class Bolt11Invoice(
val DEFAULT_MIN_FINAL_EXPIRY_DELTA = CltvExpiryDelta(18)

private val prefixes = mapOf(
Block.RegtestGenesisBlock.hash to "lnbcrt",
Block.TestnetGenesisBlock.hash to "lntb",
Block.LivenetGenesisBlock.hash to "lnbc"
Chain.Regtest to "lnbcrt",
Chain.Testnet to "lntb",
Chain.Mainnet to "lnbc"
)

fun create(
chainHash: BlockHash,
chain: Chain,
amount: MilliSatoshi?,
paymentHash: ByteVector32,
privateKey: PrivateKey,
Expand All @@ -124,7 +126,7 @@ data class Bolt11Invoice(
extraHops: List<List<TaggedField.ExtraHop>> = listOf(),
timestampSeconds: Long = currentTimestampSeconds()
): Bolt11Invoice {
val prefix = prefixes[chainHash] ?: error("unknown chain hash")
val prefix = prefixes[chain] ?: error("unknown chain hash")
val tags = mutableListOf(
TaggedField.PaymentHash(paymentHash),
TaggedField.MinFinalCltvExpiry(minFinalCltvExpiryDelta.toLong()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment
val paymentHash = Crypto.sha256(paymentPreimage).toByteVector32()
logger.debug(mapOf("paymentHash" to paymentHash)) { "using routing hints $extraHops" }
val pr = Bolt11Invoice.create(
nodeParams.chainHash,
nodeParams.chain,
amount,
paymentHash,
nodeParams.nodePrivateKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fr.acinq.lightning.db

import fr.acinq.bitcoin.Block
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Chain
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.TxId
import fr.acinq.bitcoin.utils.Either
Expand Down Expand Up @@ -393,13 +393,13 @@ class PaymentsDbTestsCommon : LightningTestSuite() {
}

private fun createInvoice(preimage: ByteVector32): Bolt11Invoice {
return Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 150_000.msat, Crypto.sha256(preimage).toByteVector32(), randomKey(), Either.Left("invoice"), CltvExpiryDelta(16), defaultFeatures)
return Bolt11Invoice.create(Chain.Mainnet, 150_000.msat, Crypto.sha256(preimage).toByteVector32(), randomKey(), Either.Left("invoice"), CltvExpiryDelta(16), defaultFeatures)
}

private fun createExpiredInvoice(preimage: ByteVector32 = randomBytes32()): Bolt11Invoice {
val now = currentTimestampSeconds()
return Bolt11Invoice.create(
Block.LivenetGenesisBlock.hash,
Chain.Mainnet,
150_000.msat,
Crypto.sha256(preimage).toByteVector32(),
randomKey(),
Expand Down
19 changes: 18 additions & 1 deletion src/commonTest/kotlin/fr/acinq/lightning/json/JsonTestsCommon.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package fr.acinq.lightning.json

import fr.acinq.lightning.channel.states.ChannelState
import fr.acinq.lightning.channel.TestsHelper
import fr.acinq.lightning.channel.states.ChannelState
import fr.acinq.lightning.payment.Bolt11Invoice
import fr.acinq.lightning.tests.utils.LightningTestSuite
import fr.acinq.lightning.wire.OfferTypes
import kotlinx.serialization.encodeToString
import kotlin.test.Test

Expand All @@ -15,4 +17,19 @@ class JsonTestsCommon : LightningTestSuite() {
JsonSerializers.json.encodeToString(state)
}

@Test
fun `serialize bolt11 invoice`() {
val invoice =
Bolt11Invoice.read("lntb123450n1pnx4cf2pp565tka26famjckm35lakwsrtnmfk7nzwm3va2u8tdu3kw7u80n5hqcqpjsp5guyuj6v84zyfxm3ae8y49rffgkcxsky73hun8mwvqfjdxw46ea0q9q7sqqqqqqqqqqqqqqqqqqqsqqqqqysgqdq523jhxapqd9h8vmmfvdjsmqz9gxqyjw5qrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cnfl6m5dzjwjw4hyqqqqlgqqqqqeqqjqedwgzyf2kuzyu4erj4xhdtknc9d8y8xkt8z80cpulg2q0mvgdcv4e7mgntf2nhur0x72k57ql7zx8ydzwtrxcnx9nk4pj65vfnhd3hsqyzlyxm")
.get()
JsonSerializers.json.encodeToString(JsonSerializers.Bolt11InvoiceSerializer, invoice)
}

@Test
fun `serialize bolt12 offer`() {
val offer =
OfferTypes.Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqsespexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqrt2gkjvf2rj2vnt7m7chnmazen8wpur2h65ttgftkqaugy6ql9dcsyq39xc2g084xfn0s50zlh2ex22vvaqxqz3vmudklz453nns4d0624sqr8ux4p5usm22qevld4ydfck7hwgcg9wc3f78y7jqhc6hwdq7e9dwkhty3svq5ju4dptxtldjumlxh5lw48jsz6pnagtwrmeus7uq9rc5g6uddwcwldpklxexvlezld8egntua4gsqqy8auz966nksacdac8yv3maq6elp")
.get()
JsonSerializers.json.encodeToString(JsonSerializers.OfferSerializer, offer)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import fr.acinq.lightning.channel.remove
import fr.acinq.lightning.tests.utils.LightningTestSuite
import fr.acinq.lightning.utils.*
import fr.acinq.secp256k1.Hex
import kotlinx.datetime.Clock
import kotlin.random.Random
import kotlin.test.*

class Bolt11InvoiceTestsCommon : LightningTestSuite() {
Expand Down Expand Up @@ -446,7 +444,7 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
mapOf(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.ShutdownAnySegwit to FeatureSupport.Optional),
setOf(UnknownFeature(103), UnknownFeature(256))
)
val pr = Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 500.msat, randomBytes32(), randomKey(), Either.Left("non-invoice features"), CltvExpiryDelta(6), nodeFeatures)
val pr = Bolt11Invoice.create(Chain.Mainnet, 500.msat, randomBytes32(), randomKey(), Either.Left("non-invoice features"), CltvExpiryDelta(6), nodeFeatures)
assertEquals(Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory), pr.features)
}

Expand Down Expand Up @@ -492,7 +490,7 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
@Test
fun `payment secret`() {
val features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.BasicMultiPartPayment to FeatureSupport.Optional)
val pr = Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, priv, Either.Left("Some invoice"), CltvExpiryDelta(18), features)
val pr = Bolt11Invoice.create(Chain.Mainnet, 123.msat, ByteVector32.One, priv, Either.Left("Some invoice"), CltvExpiryDelta(18), features)
assertNotNull(pr.paymentSecret)
assertEquals(ByteVector("024100"), pr.features.toByteArray().toByteVector())

Expand All @@ -508,7 +506,7 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
// Invoices must use a payment secret.
assertFails {
Bolt11Invoice.create(
Block.LivenetGenesisBlock.hash,
Chain.Mainnet,
123.msat,
ByteVector32.One,
priv,
Expand All @@ -523,7 +521,7 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
fun `invoice with descriptionHash`() {
val descriptionHash = randomBytes32()
val features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.BasicMultiPartPayment to FeatureSupport.Optional)
val pr = Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, priv, Either.Right(descriptionHash), CltvExpiryDelta(18), features)
val pr = Bolt11Invoice.create(Chain.Mainnet, 123.msat, ByteVector32.One, priv, Either.Right(descriptionHash), CltvExpiryDelta(18), features)
assertNotNull(pr.descriptionHash)
assertNull(pr.description)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package fr.acinq.lightning.payment

import fr.acinq.bitcoin.Block
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.PrivateKey
import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.*
import fr.acinq.lightning.Lightning.randomBytes32
Expand Down Expand Up @@ -246,7 +243,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() {
Feature.TrampolinePayment to FeatureSupport.Optional
)
Bolt11Invoice.create(
chainHash = Block.LivenetGenesisBlock.hash,
chain = Chain.Mainnet,
amount = 195_000.msat,
paymentHash = randomBytes32(),
privateKey = recipientKey,
Expand Down Expand Up @@ -914,7 +911,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() {
}

return Bolt11Invoice.create(
chainHash = Block.LivenetGenesisBlock.hash,
chain = Chain.Mainnet,
amount = amount,
paymentHash = paymentHash,
privateKey = privKey,
Expand Down

0 comments on commit f739e46

Please sign in to comment.