All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.fr.acinq.lightning.wire.PaymentOnion.kt Maven / Gradle / Ivy

package fr.acinq.lightning.wire

import fr.acinq.bitcoin.ByteVector
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.PublicKey
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.flatMap
import fr.acinq.lightning.CltvExpiry
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.payment.Bolt11Invoice
import fr.acinq.lightning.payment.Bolt12Invoice
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.toByteVector

sealed class OnionPaymentPayloadTlv : Tlv {
    /** Amount to forward to the next node. */
    data class AmountToForward(val amount: MilliSatoshi) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = AmountToForward.tag
        override fun write(out: Output) = LightningCodecs.writeTU64(amount.toLong(), out)

        companion object : TlvValueReader {
            const val tag: Long = 2
            override fun read(input: Input): AmountToForward = AmountToForward(MilliSatoshi(LightningCodecs.tu64(input)))
        }
    }

    /** CLTV value to use for the HTLC offered to the next node. */
    data class OutgoingCltv(val cltv: CltvExpiry) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = OutgoingCltv.tag
        override fun write(out: Output) = LightningCodecs.writeTU32(cltv.toLong().toInt(), out)

        companion object : TlvValueReader {
            const val tag: Long = 4
            override fun read(input: Input): OutgoingCltv = OutgoingCltv(CltvExpiry(LightningCodecs.tu32(input).toLong()))
        }
    }

    /** Id of the channel to use to forward a payment to the next node. */
    data class OutgoingChannelId(val shortChannelId: ShortChannelId) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = OutgoingChannelId.tag
        override fun write(out: Output) = LightningCodecs.writeU64(shortChannelId.toLong(), out)

        companion object : TlvValueReader {
            const val tag: Long = 6
            override fun read(input: Input): OutgoingChannelId = OutgoingChannelId(ShortChannelId(LightningCodecs.u64(input)))
        }
    }

    /**
     * Bolt 11 payment details (only included for the last node).
     *
     * @param secret payment secret specified in the Bolt 11 invoice.
     * @param totalAmount total amount in multi-part payments. When missing, assumed to be equal to AmountToForward.
     */
    data class PaymentData(val secret: ByteVector32, val totalAmount: MilliSatoshi) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = PaymentData.tag
        override fun write(out: Output) {
            LightningCodecs.writeBytes(secret, out)
            LightningCodecs.writeTU64(totalAmount.toLong(), out)
        }

        companion object : TlvValueReader {
            const val tag: Long = 8
            override fun read(input: Input): PaymentData = PaymentData(ByteVector32(LightningCodecs.bytes(input, 32)), MilliSatoshi(LightningCodecs.tu64(input)))
        }
    }

    /**
     * Route blinding lets the recipient provide some encrypted data for each intermediate node in the blinded part of
     * the route. This data cannot be decrypted or modified by the sender and usually contains information to locate the
     * next node without revealing it to the sender.
     */
    data class EncryptedRecipientData(val data: ByteVector) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = EncryptedRecipientData.tag
        override fun write(out: Output) {
            LightningCodecs.writeBytes(data, out)
        }

        companion object : TlvValueReader {
            const val tag: Long = 10
            override fun read(input: Input): EncryptedRecipientData = EncryptedRecipientData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())
        }
    }

    /** Blinding ephemeral public key for the introduction node of a blinded route. */
    data class BlindingPoint(val publicKey: PublicKey) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = BlindingPoint.tag
        override fun write(out: Output) {
            LightningCodecs.writeBytes(publicKey.value, out)
        }

        companion object : TlvValueReader {
            const val tag: Long = 12
            override fun read(input: Input): BlindingPoint = BlindingPoint(PublicKey(LightningCodecs.bytes(input, 33)))
        }
    }

    /**
     * When payment metadata is included in a Bolt 9 invoice, we should send it as-is to the recipient.
     * This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
     */
    data class PaymentMetadata(val data: ByteVector) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = PaymentMetadata.tag
        override fun write(out: Output) = LightningCodecs.writeBytes(data, out)

        companion object : TlvValueReader {
            const val tag: Long = 16
            override fun read(input: Input): PaymentMetadata = PaymentMetadata(ByteVector(LightningCodecs.bytes(input, input.availableBytes)))
        }
    }

    /** Total amount in blinded multi-part payments. */
    data class TotalAmount(val totalAmount: MilliSatoshi) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = TotalAmount.tag
        override fun write(out: Output) {
            LightningCodecs.writeTU64(totalAmount.toLong(), out)
        }

        companion object : TlvValueReader {
            const val tag: Long = 18
            override fun read(input: Input): TotalAmount = TotalAmount(MilliSatoshi(LightningCodecs.tu64(input)))
        }
    }

    /**
     * Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
     * because the final recipient doesn't support trampoline.
     */
    data class InvoiceFeatures(val features: ByteVector) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = InvoiceFeatures.tag
        override fun write(out: Output) = LightningCodecs.writeBytes(features, out)

        companion object : TlvValueReader {
            const val tag: Long = 66097
            override fun read(input: Input): InvoiceFeatures = InvoiceFeatures(ByteVector(LightningCodecs.bytes(input, input.availableBytes)))
        }
    }

    /** Id of the next node. */
    data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = OutgoingNodeId.tag
        override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)

        companion object : TlvValueReader {
            const val tag: Long = 66098
            override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
        }
    }

    /**
     * Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment
     * because the final recipient doesn't support trampoline.
     */
    data class InvoiceRoutingInfo(val extraHops: List>) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = InvoiceRoutingInfo.tag
        override fun write(out: Output) {
            for (routeHint in extraHops) {
                LightningCodecs.writeByte(routeHint.size, out)
                routeHint.map {
                    LightningCodecs.writeBytes(it.nodeId.value, out)
                    LightningCodecs.writeU64(it.shortChannelId.toLong(), out)
                    LightningCodecs.writeU32(it.feeBase.toLong().toInt(), out)
                    LightningCodecs.writeU32(it.feeProportionalMillionths.toInt(), out)
                    LightningCodecs.writeU16(it.cltvExpiryDelta.toInt(), out)
                }
            }
        }

        companion object : TlvValueReader {
            const val tag: Long = 66099
            override fun read(input: Input): InvoiceRoutingInfo {
                val extraHops = mutableListOf>()
                while (input.availableBytes > 0) {
                    val hopCount = LightningCodecs.byte(input)
                    val extraHop = (0 until hopCount).map {
                        Bolt11Invoice.TaggedField.ExtraHop(
                            PublicKey(LightningCodecs.bytes(input, 33)),
                            ShortChannelId(LightningCodecs.u64(input)),
                            MilliSatoshi(LightningCodecs.u32(input).toLong()),
                            LightningCodecs.u32(input).toLong(),
                            CltvExpiryDelta(LightningCodecs.u16(input))
                        )
                    }
                    extraHops.add(extraHop)
                }
                return InvoiceRoutingInfo(extraHops)
            }
        }
    }

    /** An encrypted trampoline onion packet. */
    data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = TrampolineOnion.tag
        override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)

        companion object : TlvValueReader {
            const val tag: Long = 66100
            override fun read(input: Input): TrampolineOnion {
                val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
                return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
            }
        }
    }

    /** Blinded paths to relay the payment to */
    data class OutgoingBlindedPaths(val paths: List) : OnionPaymentPayloadTlv() {
        override val tag: Long get() = OutgoingBlindedPaths.tag
        override fun write(out: Output) {
            for (path in paths) {
                OfferTypes.writePath(path.route, out)
                OfferTypes.writePaymentInfo(path.paymentInfo, out)
            }
        }

        companion object : TlvValueReader {
            const val tag: Long = 66102
            override fun read(input: Input): OutgoingBlindedPaths {
                val paths = ArrayList()
                while (input.availableBytes > 0) {
                    val route = OfferTypes.readPath(input)
                    val payInfo = OfferTypes.readPaymentInfo(input)
                    paths.add(Bolt12Invoice.Companion.PaymentBlindedContactInfo(route, payInfo))
                }
                return OutgoingBlindedPaths(paths)
            }
        }
    }

}

object PaymentOnion {

    sealed class PerHopPayload {

        abstract fun write(out: Output)

        fun write(): ByteArray {
            val out = ByteArrayOutput()
            write(out)
            return out.toByteArray()
        }

        companion object {
            val tlvSerializer = TlvStreamSerializer(
                true, @Suppress("UNCHECKED_CAST") mapOf(
                    OnionPaymentPayloadTlv.AmountToForward.tag to OnionPaymentPayloadTlv.AmountToForward.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.OutgoingCltv.tag to OnionPaymentPayloadTlv.OutgoingCltv.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.OutgoingChannelId.tag to OnionPaymentPayloadTlv.OutgoingChannelId.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.PaymentData.tag to OnionPaymentPayloadTlv.PaymentData.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.EncryptedRecipientData.tag to OnionPaymentPayloadTlv.EncryptedRecipientData.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.BlindingPoint.tag to OnionPaymentPayloadTlv.BlindingPoint.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.PaymentMetadata.tag to OnionPaymentPayloadTlv.PaymentMetadata.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.TotalAmount.tag to OnionPaymentPayloadTlv.TotalAmount.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.InvoiceFeatures.tag to OnionPaymentPayloadTlv.InvoiceFeatures.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader,
                    OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag to OnionPaymentPayloadTlv.OutgoingBlindedPaths.Companion as TlvValueReader,
                )
            )

            fun read(input: Input): Either> = try {
                Either.Right(tlvSerializer.read(input))
            } catch (_: Throwable) {
                // We should change TlvStream.read to return an Either>, which allows returning a more accurate error here.
                Either.Left(InvalidOnionPayload(0, 0))
            }

            fun read(bytes: ByteArray): Either> = read(ByteArrayInput(bytes))
        }
    }

    interface PerHopPayloadReader {
        fun read(input: Input): Either
        fun read(bytes: ByteArray): Either = read(ByteArrayInput(bytes))
        fun read(bytes: ByteVector): Either = read(bytes.toByteArray())
    }

    sealed class FinalPayload : PerHopPayload() {
        abstract val amount: MilliSatoshi
        abstract val totalAmount: MilliSatoshi
        abstract val expiry: CltvExpiry

        data class Standard(val records: TlvStream) : FinalPayload() {
            override val amount = records.get()!!.amount
            override val expiry = records.get()!!.cltv
            val paymentSecret = records.get()!!.secret
            override val totalAmount = run {
                val total = records.get()!!.totalAmount
                if (total > 0.msat) total else amount
            }
            val paymentMetadata = records.get()?.data

            override fun write(out: Output) = tlvSerializer.write(records, out)

            companion object : PerHopPayloadReader {
                override fun read(input: Input): Either {
                    return PerHopPayload.read(input).flatMap { tlvs ->
                        when {
                            tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0))
                            tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0))
                            tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.PaymentData.tag, 0))
                            tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0))
                            else -> Either.Right(Standard(tlvs))
                        }
                    }
                }

                /** Create a single-part payment (total amount sent at once). */
                fun createSinglePartPayload(
                    amount: MilliSatoshi,
                    expiry: CltvExpiry,
                    paymentSecret: ByteVector32,
                    paymentMetadata: ByteVector?,
                    userCustomTlvs: Set = setOf()
                ): Standard {
                    val tlvs = buildSet {
                        add(OnionPaymentPayloadTlv.AmountToForward(amount))
                        add(OnionPaymentPayloadTlv.OutgoingCltv(expiry))
                        add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, amount))
                        paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
                    }
                    return Standard(TlvStream(tlvs, userCustomTlvs))
                }

                /** Create a partial payment (total amount split between multiple payments). */
                fun createMultiPartPayload(
                    amount: MilliSatoshi,
                    totalAmount: MilliSatoshi,
                    expiry: CltvExpiry,
                    paymentSecret: ByteVector32,
                    paymentMetadata: ByteVector?,
                    additionalTlvs: Set = setOf(),
                    userCustomTlvs: Set = setOf()
                ): Standard {
                    val tlvs = buildSet {
                        add(OnionPaymentPayloadTlv.AmountToForward(amount))
                        add(OnionPaymentPayloadTlv.OutgoingCltv(expiry))
                        add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount))
                        paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
                        addAll(additionalTlvs)
                    }
                    return Standard(TlvStream(tlvs, userCustomTlvs))
                }

                /** Create a trampoline outer payload. */
                fun createTrampolinePayload(
                    amount: MilliSatoshi,
                    totalAmount: MilliSatoshi,
                    expiry: CltvExpiry,
                    paymentSecret: ByteVector32,
                    trampolinePacket: OnionRoutingPacket
                ): Standard {
                    val tlvs = TlvStream(
                        OnionPaymentPayloadTlv.AmountToForward(amount),
                        OnionPaymentPayloadTlv.OutgoingCltv(expiry),
                        OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount),
                        OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)
                    )
                    return Standard(tlvs)
                }
            }
        }

        data class Blinded(val records: TlvStream, val recipientData: RouteBlindingEncryptedData) : FinalPayload() {
            override val amount = records.get()!!.amount
            override val totalAmount = records.get()!!.totalAmount
            override val expiry = records.get()!!.cltv
            val pathId = recipientData.pathId!!

            override fun write(out: Output) = tlvSerializer.write(records, out)

            companion object {
                fun validate(records: TlvStream, blindedRecords: RouteBlindingEncryptedData): Either {
                    // Bolt 4: MUST return an error if the payload contains other tlv fields than `encrypted_recipient_data`, `current_blinding_point`, `amt_to_forward`, `outgoing_cltv_value` and `total_amount_msat`.
                    val allowed = setOf(
                        OnionPaymentPayloadTlv.AmountToForward.tag,
                        OnionPaymentPayloadTlv.OutgoingCltv.tag,
                        OnionPaymentPayloadTlv.EncryptedRecipientData.tag,
                        OnionPaymentPayloadTlv.BlindingPoint.tag,
                        OnionPaymentPayloadTlv.TotalAmount.tag,
                    )
                    return when {
                        records.get() == null -> Either.Left(MissingRequiredTlv(OnionPaymentPayloadTlv.AmountToForward.tag))
                        records.get() == null -> Either.Left(MissingRequiredTlv(OnionPaymentPayloadTlv.OutgoingCltv.tag))
                        records.get() == null -> Either.Left(MissingRequiredTlv(OnionPaymentPayloadTlv.EncryptedRecipientData.tag))
                        records.get() == null -> Either.Left(MissingRequiredTlv(OnionPaymentPayloadTlv.TotalAmount.tag))
                        records.records.any { !allowed.contains(it.tag) } -> Either.Left(ForbiddenTlv(records.records.first { !allowed.contains(it.tag) }.tag))
                        records.unknown.isNotEmpty() -> Either.Left(ForbiddenTlv(records.unknown.first().tag))
                        blindedRecords.pathId == null -> Either.Left(MissingRequiredTlv(RouteBlindingEncryptedDataTlv.PathId.tag))
                        else -> Either.Right(Blinded(records, blindedRecords))
                    }
                }
            }
        }
    }

    data class ChannelRelayPayload(val records: TlvStream) : PerHopPayload() {
        val amountToForward = records.get()!!.amount
        val outgoingCltv = records.get()!!.cltv
        val outgoingChannelId = records.get()!!.shortChannelId

        override fun write(out: Output) = tlvSerializer.write(records, out)

        companion object : PerHopPayloadReader {
            override fun read(input: Input): Either {
                return PerHopPayload.read(input).flatMap { tlvs ->
                    when {
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingChannelId.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0))
                        else -> Either.Right(ChannelRelayPayload(tlvs))
                    }
                }
            }

            fun create(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry): ChannelRelayPayload =
                ChannelRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId)))
        }
    }

    data class NodeRelayPayload(val records: TlvStream) : PerHopPayload() {
        val amountToForward = records.get()!!.amount
        val outgoingCltv = records.get()!!.cltv
        val outgoingNodeId = records.get()!!.nodeId
        val totalAmount = run {
            val paymentData = records.get()
            when {
                paymentData == null -> amountToForward
                paymentData.totalAmount == MilliSatoshi(0) -> amountToForward
                else -> paymentData.totalAmount
            }
        }

        override fun write(out: Output) = tlvSerializer.write(records, out)

        companion object : PerHopPayloadReader {
            override fun read(input: Input): Either {
                return PerHopPayload.read(input).flatMap { tlvs ->
                    when {
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingNodeId.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.BlindingPoint.tag, 0))
                        else -> Either.Right(NodeRelayPayload(tlvs))
                    }
                }
            }

            fun create(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey) =
                NodeRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amount), OnionPaymentPayloadTlv.OutgoingCltv(expiry), OnionPaymentPayloadTlv.OutgoingNodeId(nextNodeId)))
        }
    }

    data class RelayToNonTrampolinePayload(val records: TlvStream) : PerHopPayload() {
        val amountToForward = records.get()!!.amount
        val outgoingCltv = records.get()!!.cltv
        val outgoingNodeId = records.get()!!.nodeId
        val totalAmount = run {
            val paymentData = records.get()
            when {
                paymentData == null -> amountToForward
                paymentData.totalAmount == MilliSatoshi(0) -> amountToForward
                else -> paymentData.totalAmount
            }
        }

        // NB: the following fields are only included in the trampoline-to-legacy case.
        val paymentSecret = records.get()!!.secret
        val paymentMetadata = records.get()?.data
        val invoiceFeatures = records.get()!!.features
        val invoiceRoutingInfo = records.get()!!.extraHops

        override fun write(out: Output) = tlvSerializer.write(records, out)

        companion object : PerHopPayloadReader {
            override fun read(input: Input): Either {
                return PerHopPayload.read(input).flatMap { tlvs ->
                    when {
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingNodeId.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.PaymentData.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.InvoiceFeatures.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.BlindingPoint.tag, 0))
                        else -> Either.Right(RelayToNonTrampolinePayload(tlvs))
                    }
                }
            }

            fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): RelayToNonTrampolinePayload =
                RelayToNonTrampolinePayload(
                    TlvStream(
                        buildSet {
                            add(OnionPaymentPayloadTlv.AmountToForward(amount))
                            add(OnionPaymentPayloadTlv.OutgoingCltv(expiry))
                            add(OnionPaymentPayloadTlv.OutgoingNodeId(targetNodeId))
                            add(OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret, totalAmount))
                            invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
                            add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector()))
                            add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(invoice.routingInfo.map { it.hints }))
                        }
                    )
                )
        }
    }

    data class RelayToBlindedPayload(val records: TlvStream) : PerHopPayload() {
        val amountToForward = records.get()!!.amount
        val outgoingCltv = records.get()!!.cltv
        val outgoingBlindedPaths = records.get()!!.paths
        val invoiceFeatures = records.get()!!.features

        override fun write(out: Output) = tlvSerializer.write(records, out)

        companion object : PerHopPayloadReader {
            override fun read(input: Input): Either {
                return PerHopPayload.read(input).flatMap { tlvs ->
                    when {
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.InvoiceFeatures.tag, 0))
                        tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0))
                        tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.BlindingPoint.tag, 0))
                        else -> Either.Right(RelayToBlindedPayload(tlvs))
                    }
                }
            }

            fun create(amount: MilliSatoshi, expiry: CltvExpiry, invoice: Bolt12Invoice): RelayToBlindedPayload =
                RelayToBlindedPayload(
                    TlvStream(
                        setOf(
                            OnionPaymentPayloadTlv.AmountToForward(amount),
                            OnionPaymentPayloadTlv.OutgoingCltv(expiry),
                            OnionPaymentPayloadTlv.OutgoingBlindedPaths(invoice.blindedPaths),
                            OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector())
                        )
                    )
                )
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy