mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-19 09:54:02 +01:00
Add Bolt12 types and codecs (#2145)
Add types for representing offers and Bolt12 invoices (lightning/bolts#798)
This commit is contained in:
parent
787c51acc2
commit
c7c515a0ed
@ -62,6 +62,8 @@ case class UnknownFeature(bitIndex: Int)
|
|||||||
|
|
||||||
case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
|
case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
|
||||||
|
|
||||||
|
def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty
|
||||||
|
|
||||||
def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match {
|
def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match {
|
||||||
case Some(s) => activated.get(feature).contains(s)
|
case Some(s) => activated.get(feature).contains(s)
|
||||||
case None => activated.contains(feature)
|
case None => activated.contains(feature)
|
||||||
|
@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
|
|||||||
import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute}
|
import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute}
|
||||||
import fr.acinq.eclair.db._
|
import fr.acinq.eclair.db._
|
||||||
import fr.acinq.eclair.db.pg.PgUtils.PgLock
|
import fr.acinq.eclair.db.pg.PgUtils.PgLock
|
||||||
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent}
|
import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent}
|
||||||
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
|
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
|
||||||
import grizzled.slf4j.Logging
|
import grizzled.slf4j.Logging
|
||||||
import scodec.bits.BitVector
|
import scodec.bits.BitVector
|
||||||
@ -32,7 +32,6 @@ import java.sql.{Connection, ResultSet, Statement, Timestamp}
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
import scala.concurrent.duration.DurationLong
|
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
object PgPaymentsDb {
|
object PgPaymentsDb {
|
||||||
@ -157,7 +156,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
|
|||||||
MilliSatoshi(rs.getLong("recipient_amount_msat")),
|
MilliSatoshi(rs.getLong("recipient_amount_msat")),
|
||||||
PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
|
PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
|
||||||
TimestampMilli(rs.getTimestamp("created_at").getTime),
|
TimestampMilli(rs.getTimestamp("created_at").getTime),
|
||||||
rs.getStringNullable("payment_request").map(Invoice.fromString),
|
rs.getStringNullable("payment_request").map(Invoice.fromString(_).get),
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -249,7 +248,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
|
|||||||
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
|
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
|
||||||
val invoice = rs.getString("payment_request")
|
val invoice = rs.getString("payment_request")
|
||||||
IncomingPayment(
|
IncomingPayment(
|
||||||
Bolt11Invoice.fromString(invoice),
|
Bolt11Invoice.fromString(invoice).get,
|
||||||
rs.getByteVector32FromHex("payment_preimage"),
|
rs.getByteVector32FromHex("payment_preimage"),
|
||||||
rs.getString("payment_type"),
|
rs.getString("payment_type"),
|
||||||
TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")),
|
TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")),
|
||||||
|
@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
|
|||||||
import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute}
|
import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute}
|
||||||
import fr.acinq.eclair.db._
|
import fr.acinq.eclair.db._
|
||||||
import fr.acinq.eclair.db.sqlite.SqliteUtils._
|
import fr.acinq.eclair.db.sqlite.SqliteUtils._
|
||||||
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent}
|
import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent}
|
||||||
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
|
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
|
||||||
import grizzled.slf4j.Logging
|
import grizzled.slf4j.Logging
|
||||||
import scodec.bits.BitVector
|
import scodec.bits.BitVector
|
||||||
@ -178,7 +178,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
|
|||||||
MilliSatoshi(rs.getLong("recipient_amount_msat")),
|
MilliSatoshi(rs.getLong("recipient_amount_msat")),
|
||||||
PublicKey(rs.getByteVector("recipient_node_id")),
|
PublicKey(rs.getByteVector("recipient_node_id")),
|
||||||
TimestampMilli(rs.getLong("created_at")),
|
TimestampMilli(rs.getLong("created_at")),
|
||||||
rs.getStringNullable("payment_request").map(Invoice.fromString),
|
rs.getStringNullable("payment_request").map(Invoice.fromString(_).get),
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -256,7 +256,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
|
|||||||
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
|
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
|
||||||
val invoice = rs.getString("payment_request")
|
val invoice = rs.getString("payment_request")
|
||||||
IncomingPayment(
|
IncomingPayment(
|
||||||
Bolt11Invoice.fromString(invoice),
|
Bolt11Invoice.fromString(invoice).get,
|
||||||
rs.getByteVector32("payment_preimage"),
|
rs.getByteVector32("payment_preimage"),
|
||||||
rs.getString("payment_type"),
|
rs.getString("payment_type"),
|
||||||
TimestampMilli(rs.getLong("created_at")),
|
TimestampMilli(rs.getLong("created_at")),
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
package fr.acinq.eclair.payment
|
package fr.acinq.eclair.payment
|
||||||
|
|
||||||
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
|
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
|
||||||
import fr.acinq.bitcoin.scalacompat.{ Block, ByteVector32, ByteVector64, Crypto}
|
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
|
||||||
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32}
|
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32}
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
|
||||||
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
|
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
|
||||||
@ -30,7 +30,7 @@ import scala.util.{Failure, Success, Try}
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightning Bolt 11 invoice
|
* Lightning Bolt 11 invoice
|
||||||
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
|
* see https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
|
||||||
*
|
*
|
||||||
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
|
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
|
||||||
* @param amount_opt amount to pay (empty string means no amount is specified)
|
* @param amount_opt amount to pay (empty string means no amount is specified)
|
||||||
@ -46,8 +46,11 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
|
|||||||
amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid"))
|
amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid"))
|
||||||
require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag")
|
require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag")
|
||||||
require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag")
|
require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag")
|
||||||
private val featuresErr = Features.validateFeatureGraph(features)
|
|
||||||
require(featuresErr.isEmpty, featuresErr.map(_.message))
|
{
|
||||||
|
val featuresErr = Features.validateFeatureGraph(features)
|
||||||
|
require(featuresErr.isEmpty, featuresErr.map(_.message))
|
||||||
|
}
|
||||||
if (features.hasFeature(Features.PaymentSecret)) {
|
if (features.hasFeature(Features.PaymentSecret)) {
|
||||||
require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set")
|
require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set")
|
||||||
}
|
}
|
||||||
@ -508,13 +511,13 @@ object Bolt11Invoice {
|
|||||||
// TODO: could be optimized by preallocating the resulting buffer
|
// TODO: could be optimized by preallocating the resulting buffer
|
||||||
def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)
|
def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)
|
||||||
|
|
||||||
val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5))
|
val eight2fiveCodec: Codec[List[java.lang.Byte]] = list(ubyte(5).xmap[java.lang.Byte](b => b, b => b))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param input bech32-encoded invoice
|
* @param input bech32-encoded invoice
|
||||||
* @return a Bolt11 invoice
|
* @return a Bolt11 invoice
|
||||||
*/
|
*/
|
||||||
def fromString(input: String): Bolt11Invoice = {
|
def fromString(input: String): Try[Bolt11Invoice] = Try {
|
||||||
// used only for data validation
|
// used only for data validation
|
||||||
Bech32.decode(input, false)
|
Bech32.decode(input, false)
|
||||||
val lowercaseInput = input.toLowerCase
|
val lowercaseInput = input.toLowerCase
|
||||||
|
@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.eclair.payment
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.Bech32
|
||||||
|
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
|
||||||
|
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
|
||||||
|
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
|
||||||
|
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
|
||||||
|
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec}
|
||||||
|
import fr.acinq.eclair.wire.protocol.Offers._
|
||||||
|
import fr.acinq.eclair.wire.protocol.{Offers, TlvStream}
|
||||||
|
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond}
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightning Bolt 12 invoice
|
||||||
|
* see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
|
||||||
|
*/
|
||||||
|
case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[PublicKey]) extends Invoice {
|
||||||
|
|
||||||
|
import Bolt12Invoice._
|
||||||
|
|
||||||
|
require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount")
|
||||||
|
require(records.get[NodeId].nonEmpty, "bolt 12 invoices must provide a node id")
|
||||||
|
require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash")
|
||||||
|
require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description")
|
||||||
|
require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp")
|
||||||
|
require(records.get[Signature].nonEmpty, "bolt 12 invoices must provide a signature")
|
||||||
|
|
||||||
|
val amount: MilliSatoshi = records.get[Amount].map(_.amount).get
|
||||||
|
|
||||||
|
override val amount_opt: Option[MilliSatoshi] = Some(amount)
|
||||||
|
|
||||||
|
override val nodeId: Crypto.PublicKey = nodeId_opt.getOrElse(records.get[NodeId].get.nodeId1)
|
||||||
|
|
||||||
|
override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash
|
||||||
|
|
||||||
|
override val paymentSecret: Option[ByteVector32] = None
|
||||||
|
|
||||||
|
override val paymentMetadata: Option[ByteVector] = None
|
||||||
|
|
||||||
|
override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description)
|
||||||
|
|
||||||
|
override val routingInfo: Seq[Seq[ExtraHop]] = Seq.empty
|
||||||
|
|
||||||
|
override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp
|
||||||
|
|
||||||
|
override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA)
|
||||||
|
|
||||||
|
override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
|
||||||
|
|
||||||
|
override def toString: String = {
|
||||||
|
val data = invoiceCodec.encode(this).require.bytes
|
||||||
|
Bech32.encodeBytes(hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
|
||||||
|
|
||||||
|
val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId)
|
||||||
|
|
||||||
|
val blindedPaths: Option[Seq[RouteBlinding.BlindedRoute]] = records.get[Paths].map(_.paths)
|
||||||
|
|
||||||
|
val issuer: Option[String] = records.get[Issuer].map(_.issuer)
|
||||||
|
|
||||||
|
val quantity: Option[Long] = records.get[Quantity].map(_.quantity)
|
||||||
|
|
||||||
|
val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash)
|
||||||
|
|
||||||
|
val payerKey: Option[ByteVector32] = records.get[PayerKey].map(_.publicKey)
|
||||||
|
|
||||||
|
val payerNote: Option[String] = records.get[PayerNote].map(_.note)
|
||||||
|
|
||||||
|
val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info)
|
||||||
|
|
||||||
|
val fallbacks: Option[Seq[FallbackAddress]] = records.get[Fallbacks].map(_.addresses)
|
||||||
|
|
||||||
|
val refundSignature: Option[ByteVector64] = records.get[RefundSignature].map(_.signature)
|
||||||
|
|
||||||
|
val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash)
|
||||||
|
|
||||||
|
val signature: ByteVector64 = records.get[Signature].get.signature
|
||||||
|
|
||||||
|
// It is assumed that the request is valid for this offer.
|
||||||
|
def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = {
|
||||||
|
Offers.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly &&
|
||||||
|
checkSignature() &&
|
||||||
|
offerId.contains(request.offerId) &&
|
||||||
|
request.chain == chain &&
|
||||||
|
!isExpired() &&
|
||||||
|
request.amount.contains(amount) &&
|
||||||
|
quantity == request.quantity_opt &&
|
||||||
|
payerKey.contains(request.payerKey) &&
|
||||||
|
payerInfo == request.payerInfo &&
|
||||||
|
// Bolt 12: MUST reject the invoice if payer_note is set, and was unset or not equal to the field in the invoice_request.
|
||||||
|
payerNote.forall(request.payerNote.contains(_)) &&
|
||||||
|
description.swap.exists(_.startsWith(offer.description)) &&
|
||||||
|
issuer == offer.issuer &&
|
||||||
|
request.features.areSupported(features)
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkRefundSignature(): Boolean = {
|
||||||
|
(refundSignature, refundFor, payerKey) match {
|
||||||
|
case (Some(sig), Some(hash), Some(key)) => verifySchnorr(signatureTag("payer_signature"), hash, sig, key)
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkSignature(): Boolean = {
|
||||||
|
verifySchnorr(signatureTag("signature"), rootHash(Offers.removeSignature(records), invoiceTlvCodec), signature, Offers.xOnlyPublicKey(nodeId))
|
||||||
|
}
|
||||||
|
|
||||||
|
def withNodeId(nodeId: PublicKey): Bolt12Invoice = Bolt12Invoice(records, Some(nodeId))
|
||||||
|
}
|
||||||
|
|
||||||
|
object Bolt12Invoice {
|
||||||
|
val hrp = "lni"
|
||||||
|
val DEFAULT_EXPIRY_SECONDS: Long = 7200
|
||||||
|
val DEFAULT_MIN_FINAL_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an invoice for a given offer and invoice request.
|
||||||
|
*
|
||||||
|
* @param offer the offer this invoice corresponds to
|
||||||
|
* @param request the request this invoice responds to
|
||||||
|
* @param preimage the preimage to use for the payment
|
||||||
|
* @param nodeKey the key that was used to generate the offer, may be different from our public nodeId if we're hiding behind a blinded route
|
||||||
|
* @param features invoice features
|
||||||
|
*/
|
||||||
|
def apply(offer: Offer, request: InvoiceRequest, preimage: ByteVector32, nodeKey: PrivateKey, features: Features[InvoiceFeature]): Bolt12Invoice = {
|
||||||
|
require(request.amount.nonEmpty || offer.amount.nonEmpty)
|
||||||
|
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||||
|
Some(Chain(request.chain)),
|
||||||
|
Some(CreatedAt(TimestampSecond.now())),
|
||||||
|
Some(PaymentHash(Crypto.sha256(preimage))),
|
||||||
|
Some(OfferId(offer.offerId)),
|
||||||
|
Some(NodeId(nodeKey.publicKey)),
|
||||||
|
Some(Amount(request.amount.orElse(offer.amount.map(_ * request.quantity)).get)),
|
||||||
|
Some(Description(offer.description)),
|
||||||
|
request.quantity_opt.map(Quantity),
|
||||||
|
Some(PayerKey(request.payerKey)),
|
||||||
|
request.payerInfo.map(PayerInfo),
|
||||||
|
request.payerNote.map(PayerNote),
|
||||||
|
request.replaceInvoice.map(ReplaceInvoice),
|
||||||
|
offer.issuer.map(Issuer),
|
||||||
|
Some(FeaturesTlv(features.unscoped()))
|
||||||
|
).flatten
|
||||||
|
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||||
|
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), Some(nodeKey.publicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
def signatureTag(fieldName: String): String = "lightning" + "invoice" + fieldName
|
||||||
|
|
||||||
|
def fromString(input: String): Try[Bolt12Invoice] = Try {
|
||||||
|
val triple = Bech32.decodeBytes(input.toLowerCase, true)
|
||||||
|
val prefix = triple.getFirst
|
||||||
|
val encoded = triple.getSecond
|
||||||
|
val encoding = triple.getThird
|
||||||
|
require(prefix == hrp)
|
||||||
|
require(encoding == Bech32.Encoding.Beck32WithoutChecksum)
|
||||||
|
invoiceCodec.decode(ByteVector(encoded).bits).require.value
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi,
|
|||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import scala.concurrent.duration.FiniteDuration
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
trait Invoice {
|
trait Invoice {
|
||||||
val amount_opt: Option[MilliSatoshi]
|
val amount_opt: Option[MilliSatoshi]
|
||||||
@ -52,7 +53,11 @@ trait Invoice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Invoice {
|
object Invoice {
|
||||||
def fromString(input: String): Invoice = {
|
def fromString(input: String): Try[Invoice] = {
|
||||||
Bolt11Invoice.fromString(input)
|
if (input.toLowerCase.startsWith("lni")) {
|
||||||
|
Bolt12Invoice.fromString(input)
|
||||||
|
} else {
|
||||||
|
Bolt11Invoice.fromString(input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ object Scripts {
|
|||||||
def encodeNumber(n: Long): ScriptElt = n match {
|
def encodeNumber(n: Long): ScriptElt = n match {
|
||||||
case 0 => OP_0
|
case 0 => OP_0
|
||||||
case -1 => OP_1NEGATE
|
case -1 => OP_1NEGATE
|
||||||
case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt)
|
case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt).get
|
||||||
case _ => OP_PUSHDATA(Script.encodeNumber(n))
|
case _ => OP_PUSHDATA(Script.encodeNumber(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ package fr.acinq.eclair.wire.protocol
|
|||||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||||
import fr.acinq.eclair.UInt64
|
import fr.acinq.eclair.UInt64
|
||||||
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute}
|
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute}
|
||||||
|
import fr.acinq.eclair.payment.Bolt12Invoice
|
||||||
|
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceErrorCodec, invoiceRequestCodec}
|
||||||
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv}
|
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv}
|
||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
@ -40,6 +42,24 @@ object OnionMessagePayloadTlv {
|
|||||||
*/
|
*/
|
||||||
case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv
|
case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to pay a Bolt 12 offer, we must send an onion message to request an invoice corresponding to that offer.
|
||||||
|
* The creator of the offer will send us an invoice back through our blinded reply path.
|
||||||
|
*/
|
||||||
|
case class InvoiceRequest(request: Offers.InvoiceRequest) extends OnionMessagePayloadTlv
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When receiving an invoice request, we must send an onion message back containing an invoice corresponding to the
|
||||||
|
* requested offer (if it was an offer we published).
|
||||||
|
*/
|
||||||
|
case class Invoice(invoice: Bolt12Invoice) extends OnionMessagePayloadTlv
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This message may be used when we receive an invalid invoice or invoice request.
|
||||||
|
* It contains information helping senders figure out why their message was invalid.
|
||||||
|
*/
|
||||||
|
case class InvoiceError(error: Offers.InvoiceError) extends OnionMessagePayloadTlv
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object MessageOnion {
|
object MessageOnion {
|
||||||
@ -62,6 +82,9 @@ object MessageOnion {
|
|||||||
case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload {
|
case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload {
|
||||||
val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath]
|
val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath]
|
||||||
val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data
|
val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data
|
||||||
|
val invoiceRequest: Option[OnionMessagePayloadTlv.InvoiceRequest] = records.get[OnionMessagePayloadTlv.InvoiceRequest]
|
||||||
|
val invoice: Option[OnionMessagePayloadTlv.Invoice] = records.get[OnionMessagePayloadTlv.Invoice]
|
||||||
|
val invoiceError: Option[OnionMessagePayloadTlv.InvoiceError] = records.get[OnionMessagePayloadTlv.InvoiceError]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Content of the encrypted data of a final node's per-hop payload. */
|
/** Content of the encrypted data of a final node's per-hop payload. */
|
||||||
@ -90,6 +113,10 @@ object MessageOnionCodecs {
|
|||||||
private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint)
|
private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint)
|
||||||
.typecase(UInt64(2), replyPathCodec)
|
.typecase(UInt64(2), replyPathCodec)
|
||||||
.typecase(UInt64(4), encryptedDataCodec)
|
.typecase(UInt64(4), encryptedDataCodec)
|
||||||
|
.typecase(UInt64(64), variableSizeBytesLong(varintoverflow, invoiceRequestCodec.as[InvoiceRequest]))
|
||||||
|
.typecase(UInt64(66), variableSizeBytesLong(varintoverflow, invoiceCodec.as[Invoice]))
|
||||||
|
.typecase(UInt64(68), variableSizeBytesLong(varintoverflow, invoiceErrorCodec.as[InvoiceError]))
|
||||||
|
|
||||||
|
|
||||||
val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete
|
val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete
|
||||||
|
|
||||||
|
@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.eclair.wire.protocol
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||||
|
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute}
|
||||||
|
import fr.acinq.eclair.payment.Bolt12Invoice
|
||||||
|
import fr.acinq.eclair.wire.protocol.CommonCodecs._
|
||||||
|
import fr.acinq.eclair.wire.protocol.Offers._
|
||||||
|
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv
|
||||||
|
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tmillisatoshi, tu32, tu64overflow}
|
||||||
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, TimestampSecond, UInt64}
|
||||||
|
import scodec.codecs._
|
||||||
|
import scodec.{Attempt, Codec}
|
||||||
|
|
||||||
|
object OfferCodecs {
|
||||||
|
private val chains: Codec[Chains] = variableSizeBytesLong(varintoverflow, list(bytes32)).xmap[Seq[ByteVector32]](_.toSeq, _.toList).as[Chains]
|
||||||
|
|
||||||
|
private val currency: Codec[Currency] = variableSizeBytesLong(varintoverflow, utf8).as[Currency]
|
||||||
|
|
||||||
|
private val amount: Codec[Amount] = variableSizeBytesLong(varintoverflow, tmillisatoshi).as[Amount]
|
||||||
|
|
||||||
|
private val description: Codec[Description] = variableSizeBytesLong(varintoverflow, utf8).as[Description]
|
||||||
|
|
||||||
|
private val features: Codec[FeaturesTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap[Features[Feature]](Features(_), _.toByteVector).as[FeaturesTlv]
|
||||||
|
|
||||||
|
private val absoluteExpiry: Codec[AbsoluteExpiry] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[AbsoluteExpiry]
|
||||||
|
|
||||||
|
private val blindedNodeCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode]
|
||||||
|
|
||||||
|
private val pathCodec: Codec[BlindedRoute] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | listOfN(uint8, blindedNodeCodec).xmap[Seq[BlindedNode]](_.toSeq, _.toList))).as[BlindedRoute]
|
||||||
|
|
||||||
|
private val paths: Codec[Paths] = variableSizeBytesLong(varintoverflow, list(pathCodec)).xmap[Seq[BlindedRoute]](_.toSeq, _.toList).as[Paths]
|
||||||
|
|
||||||
|
private val issuer: Codec[Issuer] = variableSizeBytesLong(varintoverflow, utf8).as[Issuer]
|
||||||
|
|
||||||
|
private val quantityMin: Codec[QuantityMin] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMin]
|
||||||
|
|
||||||
|
private val quantityMax: Codec[QuantityMax] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMax]
|
||||||
|
|
||||||
|
private val nodeId: Codec[NodeId] = variableSizeBytesLong(varintoverflow, bytes32).as[NodeId]
|
||||||
|
|
||||||
|
private val sendInvoice: Codec[SendInvoice] = variableSizeBytesLong(varintoverflow, provide(SendInvoice()))
|
||||||
|
|
||||||
|
private val refundFor: Codec[RefundFor] = variableSizeBytesLong(varintoverflow, bytes32).as[RefundFor]
|
||||||
|
|
||||||
|
private val signature: Codec[Signature] = variableSizeBytesLong(varintoverflow, bytes64).as[Signature]
|
||||||
|
|
||||||
|
val offerTlvCodec: Codec[TlvStream[OfferTlv]] = TlvCodecs.tlvStream[OfferTlv](discriminated[OfferTlv].by(varint)
|
||||||
|
.typecase(UInt64(2), chains)
|
||||||
|
.typecase(UInt64(6), currency)
|
||||||
|
.typecase(UInt64(8), amount)
|
||||||
|
.typecase(UInt64(10), description)
|
||||||
|
.typecase(UInt64(12), features)
|
||||||
|
.typecase(UInt64(14), absoluteExpiry)
|
||||||
|
.typecase(UInt64(16), paths)
|
||||||
|
.typecase(UInt64(20), issuer)
|
||||||
|
.typecase(UInt64(22), quantityMin)
|
||||||
|
.typecase(UInt64(24), quantityMax)
|
||||||
|
.typecase(UInt64(30), nodeId)
|
||||||
|
.typecase(UInt64(34), refundFor)
|
||||||
|
.typecase(UInt64(54), sendInvoice)
|
||||||
|
.typecase(UInt64(240), signature)).complete
|
||||||
|
|
||||||
|
val offerCodec: Codec[Offer] = offerTlvCodec.narrow({ tlvs =>
|
||||||
|
if (tlvs.get[Description].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(10)))
|
||||||
|
}
|
||||||
|
if (tlvs.get[NodeId].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(30)))
|
||||||
|
}
|
||||||
|
Attempt.successful(Offer(tlvs))
|
||||||
|
}, {
|
||||||
|
case Offer(tlvs) => tlvs
|
||||||
|
})
|
||||||
|
|
||||||
|
private val chain: Codec[Chain] = variableSizeBytesLong(varintoverflow, bytes32).as[Chain]
|
||||||
|
|
||||||
|
private val offerId: Codec[OfferId] = variableSizeBytesLong(varintoverflow, bytes32).as[OfferId]
|
||||||
|
|
||||||
|
private val quantity: Codec[Quantity] = variableSizeBytesLong(varintoverflow, tu64overflow).as[Quantity]
|
||||||
|
|
||||||
|
private val payerKey: Codec[PayerKey] = variableSizeBytesLong(varintoverflow, bytes32).as[PayerKey]
|
||||||
|
|
||||||
|
private val payerNote: Codec[PayerNote] = variableSizeBytesLong(varintoverflow, utf8).as[PayerNote]
|
||||||
|
|
||||||
|
private val payerInfo: Codec[PayerInfo] = variableSizeBytesLong(varintoverflow, bytes).as[PayerInfo]
|
||||||
|
|
||||||
|
private val replaceInvoice: Codec[ReplaceInvoice] = variableSizeBytesLong(varintoverflow, bytes32).as[ReplaceInvoice]
|
||||||
|
|
||||||
|
val invoiceRequestTlvCodec: Codec[TlvStream[InvoiceRequestTlv]] = TlvCodecs.tlvStream[InvoiceRequestTlv](discriminated[InvoiceRequestTlv].by(varint)
|
||||||
|
.typecase(UInt64(3), chain)
|
||||||
|
.typecase(UInt64(4), offerId)
|
||||||
|
.typecase(UInt64(8), amount)
|
||||||
|
.typecase(UInt64(12), features)
|
||||||
|
.typecase(UInt64(32), quantity)
|
||||||
|
.typecase(UInt64(38), payerKey)
|
||||||
|
.typecase(UInt64(39), payerNote)
|
||||||
|
.typecase(UInt64(50), payerInfo)
|
||||||
|
.typecase(UInt64(56), replaceInvoice)
|
||||||
|
.typecase(UInt64(240), signature)).complete
|
||||||
|
|
||||||
|
val invoiceRequestCodec: Codec[InvoiceRequest] = invoiceRequestTlvCodec.narrow({ tlvs =>
|
||||||
|
if (tlvs.get[OfferId].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(4)))
|
||||||
|
} else if (tlvs.get[PayerKey].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(38)))
|
||||||
|
} else if (tlvs.get[Signature].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(240)))
|
||||||
|
} else {
|
||||||
|
Attempt.successful(InvoiceRequest(tlvs))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
case InvoiceRequest(tlvs) => tlvs
|
||||||
|
})
|
||||||
|
|
||||||
|
private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) ::
|
||||||
|
("fee_proportional_millionths" | tu32) ::
|
||||||
|
("cltv_expiry_delta" | cltvExpiryDelta)).as[PaymentInfo]
|
||||||
|
|
||||||
|
private val paymentPathsInfo: Codec[PaymentPathsInfo] = variableSizeBytesLong(varintoverflow, list(paymentInfo)).xmap[Seq[PaymentInfo]](_.toSeq, _.toList).as[PaymentPathsInfo]
|
||||||
|
|
||||||
|
private val paymentConstraints: Codec[PaymentConstraints] = (("max_cltv_expiry" | cltvExpiry) ::
|
||||||
|
("htlc_minimum_msat" | millisatoshi) ::
|
||||||
|
("allowed_features" | bytes.xmap[Features[Feature]](Features(_), _.toByteVector))).as[PaymentConstraints]
|
||||||
|
|
||||||
|
private val paymentPathsConstraints: Codec[PaymentPathsConstraints] = variableSizeBytesLong(varintoverflow, list(paymentConstraints)).xmap[Seq[PaymentConstraints]](_.toSeq, _.toList).as[PaymentPathsConstraints]
|
||||||
|
|
||||||
|
private val createdAt: Codec[CreatedAt] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[CreatedAt]
|
||||||
|
|
||||||
|
private val paymentHash: Codec[PaymentHash] = variableSizeBytesLong(varintoverflow, bytes32).as[PaymentHash]
|
||||||
|
|
||||||
|
private val relativeExpiry: Codec[RelativeExpiry] = variableSizeBytesLong(varintoverflow, tu32).as[RelativeExpiry]
|
||||||
|
|
||||||
|
private val cltv: Codec[Cltv] = variableSizeBytesLong(varintoverflow, uint16).as[CltvExpiryDelta].as[Cltv]
|
||||||
|
|
||||||
|
private val fallbackAddress: Codec[FallbackAddress] = variableSizeBytesLong(varintoverflow,
|
||||||
|
("version" | byte) ::
|
||||||
|
("address" | variableSizeBytes(uint16, bytes))).as[FallbackAddress]
|
||||||
|
|
||||||
|
private val fallbacks: Codec[Fallbacks] = variableSizeBytesLong(varintoverflow, list(fallbackAddress)).xmap[Seq[FallbackAddress]](_.toSeq, _.toList).as[Fallbacks]
|
||||||
|
|
||||||
|
private val refundSignature: Codec[RefundSignature] = variableSizeBytesLong(varintoverflow, bytes64).as[RefundSignature]
|
||||||
|
|
||||||
|
val invoiceTlvCodec: Codec[TlvStream[InvoiceTlv]] = TlvCodecs.tlvStream[InvoiceTlv](discriminated[InvoiceTlv].by(varint)
|
||||||
|
.typecase(UInt64(3), chain)
|
||||||
|
.typecase(UInt64(4), offerId)
|
||||||
|
.typecase(UInt64(8), amount)
|
||||||
|
.typecase(UInt64(10), description)
|
||||||
|
.typecase(UInt64(12), features)
|
||||||
|
// TODO: the spec for payment paths is not final, adjust codecs if changes are made to he spec.
|
||||||
|
.typecase(UInt64(16), paths)
|
||||||
|
.typecase(UInt64(18), paymentPathsInfo)
|
||||||
|
.typecase(UInt64(19), paymentPathsConstraints)
|
||||||
|
.typecase(UInt64(20), issuer)
|
||||||
|
.typecase(UInt64(30), nodeId)
|
||||||
|
.typecase(UInt64(32), quantity)
|
||||||
|
.typecase(UInt64(34), refundFor)
|
||||||
|
.typecase(UInt64(38), payerKey)
|
||||||
|
.typecase(UInt64(39), payerNote)
|
||||||
|
.typecase(UInt64(40), createdAt)
|
||||||
|
.typecase(UInt64(42), paymentHash)
|
||||||
|
.typecase(UInt64(44), relativeExpiry)
|
||||||
|
.typecase(UInt64(46), cltv)
|
||||||
|
.typecase(UInt64(48), fallbacks)
|
||||||
|
.typecase(UInt64(50), payerInfo)
|
||||||
|
.typecase(UInt64(52), refundSignature)
|
||||||
|
.typecase(UInt64(56), replaceInvoice)
|
||||||
|
.typecase(UInt64(240), signature)
|
||||||
|
).complete
|
||||||
|
|
||||||
|
val invoiceCodec: Codec[Bolt12Invoice] = invoiceTlvCodec.narrow({ tlvs =>
|
||||||
|
if (tlvs.get[Amount].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(8)))
|
||||||
|
} else if (tlvs.get[Description].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(10)))
|
||||||
|
} else if (tlvs.get[NodeId].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(30)))
|
||||||
|
} else if (tlvs.get[CreatedAt].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(40)))
|
||||||
|
} else if (tlvs.get[PaymentHash].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(42)))
|
||||||
|
} else if (tlvs.get[Signature].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(240)))
|
||||||
|
} else {
|
||||||
|
Attempt.successful(Bolt12Invoice(tlvs, None))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
case Bolt12Invoice(tlvs, _) => tlvs
|
||||||
|
})
|
||||||
|
|
||||||
|
val invoiceErrorTlvCodec: Codec[TlvStream[InvoiceErrorTlv]] = TlvCodecs.tlvStream[InvoiceErrorTlv](discriminated[InvoiceErrorTlv].by(varint)
|
||||||
|
.typecase(UInt64(1), variableSizeBytesLong(varintoverflow, tu64overflow).as[ErroneousField])
|
||||||
|
.typecase(UInt64(3), variableSizeBytesLong(varintoverflow, bytes).as[SuggestedValue])
|
||||||
|
.typecase(UInt64(5), variableSizeBytesLong(varintoverflow, utf8).as[Error])
|
||||||
|
).complete
|
||||||
|
|
||||||
|
val invoiceErrorCodec: Codec[InvoiceError] = invoiceErrorTlvCodec.narrow({ tlvs =>
|
||||||
|
if (tlvs.get[Error].isEmpty) {
|
||||||
|
Attempt.failure(MissingRequiredTlv(UInt64(5)))
|
||||||
|
} else {
|
||||||
|
Attempt.successful(InvoiceError(tlvs))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
case InvoiceError(tlvs) => tlvs
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,387 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.eclair.wire.protocol
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.Bech32
|
||||||
|
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
|
||||||
|
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering}
|
||||||
|
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute
|
||||||
|
import fr.acinq.eclair.message.OnionMessages
|
||||||
|
import fr.acinq.eclair.wire.protocol.OfferCodecs._
|
||||||
|
import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv
|
||||||
|
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond}
|
||||||
|
import fr.acinq.secp256k1.Secp256k1JvmKt
|
||||||
|
import scodec.Codec
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
import scodec.codecs.vector
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightning Bolt 12 offers
|
||||||
|
* see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
|
||||||
|
*/
|
||||||
|
object Offers {
|
||||||
|
|
||||||
|
sealed trait Bolt12Tlv extends Tlv
|
||||||
|
|
||||||
|
sealed trait OfferTlv extends Bolt12Tlv
|
||||||
|
|
||||||
|
sealed trait InvoiceRequestTlv extends Bolt12Tlv
|
||||||
|
|
||||||
|
sealed trait InvoiceTlv extends Bolt12Tlv
|
||||||
|
|
||||||
|
sealed trait InvoiceErrorTlv extends Bolt12Tlv
|
||||||
|
|
||||||
|
case class Chains(chains: Seq[ByteVector32]) extends OfferTlv
|
||||||
|
|
||||||
|
case class Currency(iso4217: String) extends OfferTlv
|
||||||
|
|
||||||
|
case class Amount(amount: MilliSatoshi) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class Description(description: String) extends OfferTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class FeaturesTlv(features: Features[Feature]) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class AbsoluteExpiry(absoluteExpiry: TimestampSecond) extends OfferTlv
|
||||||
|
|
||||||
|
case class Paths(paths: Seq[BlindedRoute]) extends OfferTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class PaymentInfo(feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta)
|
||||||
|
case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class PaymentConstraints(maxCltvExpiry: CltvExpiry, minHtlc: MilliSatoshi, allowedFeatures: Features[Feature])
|
||||||
|
case class PaymentPathsConstraints(paymentConstraints: Seq[PaymentConstraints]) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class QuantityMin(min: Long) extends OfferTlv
|
||||||
|
|
||||||
|
case class QuantityMax(max: Long) extends OfferTlv
|
||||||
|
|
||||||
|
case class NodeId(xonly: ByteVector32) extends OfferTlv with InvoiceTlv {
|
||||||
|
val nodeId1: PublicKey = PublicKey(2 +: xonly)
|
||||||
|
val nodeId2: PublicKey = PublicKey(3 +: xonly)
|
||||||
|
}
|
||||||
|
|
||||||
|
object NodeId {
|
||||||
|
def apply(publicKey: PublicKey): NodeId = NodeId(xOnlyPublicKey(publicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
case class SendInvoice() extends OfferTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class RefundFor(refundedPaymentHash: ByteVector32) extends OfferTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class Signature(signature: ByteVector64) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class Chain(hash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class OfferId(offerId: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class Quantity(quantity: Long) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class PayerKey(publicKey: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
object PayerKey {
|
||||||
|
def apply(publicKey: PublicKey): PayerKey = PayerKey(xOnlyPublicKey(publicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
case class PayerNote(note: String) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class PayerInfo(info: ByteVector) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class ReplaceInvoice(paymentHash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
|
||||||
|
|
||||||
|
case class CreatedAt(timestamp: TimestampSecond) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class PaymentHash(hash: ByteVector32) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class RelativeExpiry(seconds: Long) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class Cltv(minFinalCltvExpiry: CltvExpiryDelta) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class FallbackAddress(version: Byte, value: ByteVector)
|
||||||
|
|
||||||
|
case class Fallbacks(addresses: Seq[FallbackAddress]) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class RefundSignature(signature: ByteVector64) extends InvoiceTlv
|
||||||
|
|
||||||
|
case class ErroneousField(tag: Long) extends InvoiceErrorTlv
|
||||||
|
|
||||||
|
case class SuggestedValue(value: ByteVector) extends InvoiceErrorTlv
|
||||||
|
|
||||||
|
case class Error(message: String) extends InvoiceErrorTlv
|
||||||
|
|
||||||
|
case class Offer(records: TlvStream[OfferTlv]) {
|
||||||
|
|
||||||
|
require(records.get[NodeId].nonEmpty, "bolt 12 offers must provide a node id")
|
||||||
|
require(records.get[Description].nonEmpty, "bolt 12 offers must provide a description")
|
||||||
|
|
||||||
|
val offerId: ByteVector32 = rootHash(removeSignature(records), offerTlvCodec)
|
||||||
|
|
||||||
|
val chains: Seq[ByteVector32] = records.get[Chains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash))
|
||||||
|
|
||||||
|
val currency: Option[String] = records.get[Currency].map(_.iso4217)
|
||||||
|
|
||||||
|
val amount: Option[MilliSatoshi] = currency match {
|
||||||
|
case Some(_) => None // TODO: add exchange rates
|
||||||
|
case None => records.get[Amount].map(_.amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
val description: String = records.get[Description].get.description
|
||||||
|
|
||||||
|
val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
|
||||||
|
|
||||||
|
val expiry: Option[TimestampSecond] = records.get[AbsoluteExpiry].map(_.absoluteExpiry)
|
||||||
|
|
||||||
|
val issuer: Option[String] = records.get[Issuer].map(_.issuer)
|
||||||
|
|
||||||
|
val quantityMin: Option[Long] = records.get[QuantityMin].map(_.min)
|
||||||
|
val quantityMax: Option[Long] = records.get[QuantityMax].map(_.max)
|
||||||
|
|
||||||
|
val nodeIdXOnly: ByteVector32 = records.get[NodeId].get.xonly
|
||||||
|
|
||||||
|
val sendInvoice: Boolean = records.get[SendInvoice].nonEmpty
|
||||||
|
|
||||||
|
val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash)
|
||||||
|
|
||||||
|
val signature: Option[ByteVector64] = records.get[Signature].map(_.signature)
|
||||||
|
|
||||||
|
val contact: Seq[OnionMessages.Destination] =
|
||||||
|
records.get[Paths].flatMap(_.paths.headOption).map(OnionMessages.BlindedPath).map(Seq(_))
|
||||||
|
.getOrElse(Seq(PublicKey(2.toByte +: nodeIdXOnly), PublicKey(3.toByte +: nodeIdXOnly)).map(nodeId => OnionMessages.Recipient(nodeId, None, None)))
|
||||||
|
|
||||||
|
def sign(key: PrivateKey): Offer = {
|
||||||
|
val sig = signSchnorr(Offer.signatureTag, rootHash(records, offerTlvCodec), key)
|
||||||
|
Offer(TlvStream[OfferTlv](records.records ++ Seq(Signature(sig)), records.unknown))
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkSignature(): Boolean = {
|
||||||
|
signature match {
|
||||||
|
case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, nodeIdXOnly)
|
||||||
|
case None => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def encode(): String = {
|
||||||
|
val data = offerCodec.encode(this).require.bytes
|
||||||
|
Bech32.encodeBytes(Offer.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def toString: String = encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
object Offer {
|
||||||
|
val hrp = "lno"
|
||||||
|
val signatureTag: String = "lightning" + "offer" + "signature"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param amount_opt amount if it can be determined at offer creation time.
|
||||||
|
* @param description description of the offer.
|
||||||
|
* @param nodeId the nodeId to use for this offer, which should be different from our public nodeId if we're hiding behind a blinded route.
|
||||||
|
* @param features invoice features.
|
||||||
|
* @param chain chain on which the offer is valid.
|
||||||
|
*/
|
||||||
|
def apply(amount_opt: Option[MilliSatoshi], description: String, nodeId: PublicKey, features: Features[InvoiceFeature], chain: ByteVector32): Offer = {
|
||||||
|
val tlvs: Seq[OfferTlv] = Seq(
|
||||||
|
if (chain != Block.LivenetGenesisBlock.hash) Some(Chains(Seq(chain))) else None,
|
||||||
|
amount_opt.map(Amount),
|
||||||
|
Some(Description(description)),
|
||||||
|
Some(NodeId(nodeId)),
|
||||||
|
if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None,
|
||||||
|
).flatten
|
||||||
|
Offer(TlvStream(tlvs))
|
||||||
|
}
|
||||||
|
|
||||||
|
def decode(s: String): Try[Offer] = Try {
|
||||||
|
val triple = Bech32.decodeBytes(s.toLowerCase, true)
|
||||||
|
val prefix = triple.getFirst
|
||||||
|
val encoded = triple.getSecond
|
||||||
|
val encoding = triple.getThird
|
||||||
|
require(prefix == hrp)
|
||||||
|
require(encoding == Bech32.Encoding.Beck32WithoutChecksum)
|
||||||
|
offerCodec.decode(ByteVector(encoded).bits).require.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class InvoiceRequest(records: TlvStream[InvoiceRequestTlv]) {
|
||||||
|
|
||||||
|
require(records.get[OfferId].nonEmpty, "bolt 12 invoice requests must provide an offer id")
|
||||||
|
require(records.get[PayerKey].nonEmpty, "bolt 12 invoice requests must provide a payer key")
|
||||||
|
require(records.get[Signature].nonEmpty, "bolt 12 invoice requests must provide a payer signature")
|
||||||
|
|
||||||
|
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
|
||||||
|
|
||||||
|
val offerId: ByteVector32 = records.get[OfferId].map(_.offerId).get
|
||||||
|
|
||||||
|
val amount: Option[MilliSatoshi] = records.get[Amount].map(_.amount)
|
||||||
|
|
||||||
|
val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
|
||||||
|
|
||||||
|
val quantity_opt: Option[Long] = records.get[Quantity].map(_.quantity)
|
||||||
|
|
||||||
|
val quantity: Long = quantity_opt.getOrElse(1)
|
||||||
|
|
||||||
|
val payerKey: ByteVector32 = records.get[PayerKey].get.publicKey
|
||||||
|
|
||||||
|
val payerNote: Option[String] = records.get[PayerNote].map(_.note)
|
||||||
|
|
||||||
|
val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info)
|
||||||
|
|
||||||
|
val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash)
|
||||||
|
|
||||||
|
val payerSignature: ByteVector64 = records.get[Signature].get.signature
|
||||||
|
|
||||||
|
def isValidFor(offer: Offer): Boolean = {
|
||||||
|
val amountOk = offer.amount match {
|
||||||
|
case Some(offerAmount) =>
|
||||||
|
val baseInvoiceAmount = offerAmount * quantity
|
||||||
|
amount.forall(baseInvoiceAmount <= _)
|
||||||
|
case None => amount.nonEmpty
|
||||||
|
}
|
||||||
|
amountOk &&
|
||||||
|
offer.offerId == offerId &&
|
||||||
|
offer.chains.contains(chain) &&
|
||||||
|
offer.quantityMin.forall(min => quantity_opt.nonEmpty && min <= quantity) &&
|
||||||
|
offer.quantityMax.forall(max => quantity_opt.nonEmpty && quantity <= max) &&
|
||||||
|
quantity_opt.forall(_ => offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) &&
|
||||||
|
offer.features.areSupported(features) &&
|
||||||
|
checkSignature()
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkSignature(): Boolean = {
|
||||||
|
verifySchnorr(InvoiceRequest.signatureTag, rootHash(removeSignature(records), invoiceRequestTlvCodec), payerSignature, payerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
def encode(): String = {
|
||||||
|
val data = invoiceRequestCodec.encode(this).require.bytes
|
||||||
|
Bech32.encodeBytes(InvoiceRequest.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def toString: String = encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
object InvoiceRequest {
|
||||||
|
val hrp = "lnr"
|
||||||
|
val signatureTag: String = "lightning" + "invoice_request" + "payer_signature"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a request to fetch an invoice for a given offer.
|
||||||
|
*
|
||||||
|
* @param offer Bolt 12 offer.
|
||||||
|
* @param amount amount that we want to pay.
|
||||||
|
* @param quantity quantity of items we're buying.
|
||||||
|
* @param features invoice features.
|
||||||
|
* @param payerKey private key identifying the payer: this lets us prove we're the ones who paid the invoice.
|
||||||
|
* @param chain chain we want to use to pay this offer.
|
||||||
|
*/
|
||||||
|
def apply(offer: Offer, amount: MilliSatoshi, quantity: Long, features: Features[InvoiceFeature], payerKey: PrivateKey, chain: ByteVector32): InvoiceRequest = {
|
||||||
|
require(offer.chains contains chain)
|
||||||
|
require(quantity == 1 || offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty)
|
||||||
|
val tlvs: Seq[InvoiceRequestTlv] = Seq(
|
||||||
|
Some(Chain(chain)),
|
||||||
|
Some(OfferId(offer.offerId)),
|
||||||
|
Some(Amount(amount)),
|
||||||
|
if (offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) Some(Quantity(quantity)) else None,
|
||||||
|
Some(PayerKey(payerKey.publicKey)),
|
||||||
|
Some(FeaturesTlv(features.unscoped()))
|
||||||
|
).flatten
|
||||||
|
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey)
|
||||||
|
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
|
||||||
|
}
|
||||||
|
|
||||||
|
def decode(s: String): Try[InvoiceRequest] = Try {
|
||||||
|
val triple = Bech32.decodeBytes(s.toLowerCase, true)
|
||||||
|
val prefix = triple.getFirst
|
||||||
|
val encoded = triple.getSecond
|
||||||
|
val encoding = triple.getThird
|
||||||
|
require(prefix == hrp)
|
||||||
|
require(encoding == Bech32.Encoding.Beck32WithoutChecksum)
|
||||||
|
invoiceRequestCodec.decode(ByteVector(encoded).bits).require.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class InvoiceError(records: TlvStream[InvoiceErrorTlv]) {
|
||||||
|
require(records.get[Error].nonEmpty, "bolt 12 invoice errors must provide an explanatory string")
|
||||||
|
}
|
||||||
|
|
||||||
|
def rootHash[T <: Tlv](tlvs: TlvStream[T], codec: Codec[TlvStream[T]]): ByteVector32 = {
|
||||||
|
// Encoding tlvs is always safe, unless we have a bug in our codecs, so we can call `.require` here.
|
||||||
|
val encoded = codec.encode(tlvs).require
|
||||||
|
// Decoding tlvs that we just encoded is safe as well.
|
||||||
|
// This encoding/decoding step ensures that the resulting tlvs are ordered.
|
||||||
|
val genericTlvs = vector(genericTlv).decode(encoded).require.value
|
||||||
|
val nonceKey = ByteVector("LnAll".getBytes) ++ encoded.bytes
|
||||||
|
|
||||||
|
def previousPowerOfTwo(n: Int): Int = {
|
||||||
|
var p = 1
|
||||||
|
while (p < n) {
|
||||||
|
p = p << 1
|
||||||
|
}
|
||||||
|
p >> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
def merkleTree(i: Int, j: Int): ByteVector32 = {
|
||||||
|
val (a, b) = if (j - i == 1) {
|
||||||
|
val tlv = genericTlv.encode(genericTlvs(i)).require.bytes
|
||||||
|
(hash(ByteVector("LnLeaf".getBytes), tlv), hash(nonceKey, tlv))
|
||||||
|
} else {
|
||||||
|
val k = i + previousPowerOfTwo(j - i)
|
||||||
|
(merkleTree(i, k), merkleTree(k, j))
|
||||||
|
}
|
||||||
|
if (LexicographicalOrdering.isLessThan(a, b)) {
|
||||||
|
hash(ByteVector("LnBranch".getBytes), a ++ b)
|
||||||
|
} else {
|
||||||
|
hash(ByteVector("LnBranch".getBytes), b ++ a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merkleTree(0, genericTlvs.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def hash(tag: String, msg: ByteVector): ByteVector32 = {
|
||||||
|
ByteVector.encodeAscii(tag) match {
|
||||||
|
case Right(bytes) => hash(bytes, msg)
|
||||||
|
case Left(e) => throw e // NB: the tags we use are hard-coded, so we know they're always ASCII
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def hash(tag: ByteVector, msg: ByteVector): ByteVector32 = {
|
||||||
|
val tagHash = Crypto.sha256(tag)
|
||||||
|
Crypto.sha256(tagHash ++ tagHash ++ msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
def signSchnorr(tag: String, msg: ByteVector32, key: PrivateKey): ByteVector64 = {
|
||||||
|
val h = hash(tag, msg)
|
||||||
|
// NB: we don't add auxiliary random data to keep signatures deterministic.
|
||||||
|
ByteVector64(ByteVector(Secp256k1JvmKt.getSecpk256k1.signSchnorr(h.toArray, key.value.toArray, null)))
|
||||||
|
}
|
||||||
|
|
||||||
|
def verifySchnorr(tag: String, msg: ByteVector32, signature: ByteVector64, publicKey: ByteVector32): Boolean = {
|
||||||
|
val h = hash(tag, msg)
|
||||||
|
Secp256k1JvmKt.getSecpk256k1.verifySchnorr(signature.toArray, h.toArray, publicKey.toArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
def xOnlyPublicKey(publicKey: PublicKey): ByteVector32 = ByteVector32(publicKey.value.drop(1))
|
||||||
|
|
||||||
|
/** We often need to remove the signature field to compute the merkle root. */
|
||||||
|
def removeSignature[T <: Bolt12Tlv](records: TlvStream[T]): TlvStream[T] = {
|
||||||
|
TlvStream(records.records.filter { case _: Signature => false case _ => true }, records.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -104,7 +104,7 @@ object TlvCodecs {
|
|||||||
/** Length-prefixed truncated uint16 (1 to 3 bytes unsigned integer). */
|
/** Length-prefixed truncated uint16 (1 to 3 bytes unsigned integer). */
|
||||||
val ltu16: Codec[Int] = variableSizeBytes(uint8, tu16)
|
val ltu16: Codec[Int] = variableSizeBytes(uint8, tu16)
|
||||||
|
|
||||||
private def validateGenericTlv(g: GenericTlv): Attempt[GenericTlv] = {
|
private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = {
|
||||||
if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) {
|
if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) {
|
||||||
Attempt.Failure(Err("unknown even tlv type"))
|
Attempt.Failure(Err("unknown even tlv type"))
|
||||||
} else {
|
} else {
|
||||||
@ -112,7 +112,9 @@ object TlvCodecs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv].exmap(validateGenericTlv, validateGenericTlv)
|
val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv]
|
||||||
|
|
||||||
|
private val unknownTlv = genericTlv.exmap(validateUnknownTlv, validateUnknownTlv)
|
||||||
|
|
||||||
private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match {
|
private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match {
|
||||||
case Left(generic) => generic.tag
|
case Left(generic) => generic.tag
|
||||||
@ -140,7 +142,7 @@ object TlvCodecs {
|
|||||||
* @param codec codec used for the tlv records contained in the stream.
|
* @param codec codec used for the tlv records contained in the stream.
|
||||||
* @tparam T stream namespace.
|
* @tparam T stream namespace.
|
||||||
*/
|
*/
|
||||||
def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(genericTlv, codec)).exmap(
|
def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(unknownTlv, codec)).exmap(
|
||||||
records => validateStream(codec, records),
|
records => validateStream(codec, records),
|
||||||
(stream: TlvStream[T]) => {
|
(stream: TlvStream[T]) => {
|
||||||
val records = (stream.records.map(Right(_)) ++ stream.unknown.map(Left(_))).toList
|
val records = (stream.records.map(Right(_)) ++ stream.unknown.map(Left(_))).toList
|
||||||
|
@ -171,19 +171,19 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers {
|
|||||||
|
|
||||||
test("Bolt 11 invoice") {
|
test("Bolt 11 invoice") {
|
||||||
val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy"
|
val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy"
|
||||||
val pr = Invoice.fromString(ref)
|
val pr = Invoice.fromString(ref).get
|
||||||
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}"""
|
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Bolt 11 invoice with routing hints") {
|
test("Bolt 11 invoice with routing hints") {
|
||||||
val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an"
|
val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an"
|
||||||
val pr = Invoice.fromString(ref)
|
val pr = Invoice.fromString(ref).get
|
||||||
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
|
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Bolt 11 invoice with metadata") {
|
test("Bolt 11 invoice with metadata") {
|
||||||
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0"
|
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0"
|
||||||
val pr = Invoice.fromString(ref)
|
val pr = Invoice.fromString(ref).get
|
||||||
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}"""
|
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, Mil
|
|||||||
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||||
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
|
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
|
||||||
import fr.acinq.eclair.payment.Bolt11Invoice._
|
import fr.acinq.eclair.payment.Bolt11Invoice._
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, Feature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32}
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32}
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import scodec.DecodeResult
|
import scodec.DecodeResult
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
@ -43,18 +43,18 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
// Copy of Bolt11Invoice.apply that doesn't strip unknown features
|
// Copy of Bolt11Invoice.apply that doesn't strip unknown features
|
||||||
def createInvoiceUnsafe(chainHash: ByteVector32,
|
def createInvoiceUnsafe(chainHash: ByteVector32,
|
||||||
amount: Option[MilliSatoshi],
|
amount: Option[MilliSatoshi],
|
||||||
paymentHash: ByteVector32,
|
paymentHash: ByteVector32,
|
||||||
privateKey: PrivateKey,
|
privateKey: PrivateKey,
|
||||||
description: Either[String, ByteVector32],
|
description: Either[String, ByteVector32],
|
||||||
minFinalCltvExpiryDelta: CltvExpiryDelta,
|
minFinalCltvExpiryDelta: CltvExpiryDelta,
|
||||||
fallbackAddress: Option[String] = None,
|
fallbackAddress: Option[String] = None,
|
||||||
expirySeconds: Option[Long] = None,
|
expirySeconds: Option[Long] = None,
|
||||||
extraHops: List[List[ExtraHop]] = Nil,
|
extraHops: List[List[ExtraHop]] = Nil,
|
||||||
timestamp: TimestampSecond = TimestampSecond.now(),
|
timestamp: TimestampSecond = TimestampSecond.now(),
|
||||||
paymentSecret: ByteVector32 = randomBytes32(),
|
paymentSecret: ByteVector32 = randomBytes32(),
|
||||||
paymentMetadata: Option[ByteVector] = None,
|
paymentMetadata: Option[ByteVector] = None,
|
||||||
features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = {
|
features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = {
|
||||||
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret")
|
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret")
|
||||||
val prefix = prefixes(chainHash)
|
val prefix = prefixes(chainHash)
|
||||||
val tags = {
|
val tags = {
|
||||||
@ -132,7 +132,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") {
|
test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") {
|
||||||
val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql"
|
val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt.isEmpty)
|
assert(invoice.amount_opt.isEmpty)
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -148,7 +148,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("Please send $3 for a cup of coffee to the same peer, within 1 minute") {
|
test("Please send $3 for a cup of coffee to the same peer, within 1 minute") {
|
||||||
val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh"
|
val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(250000000 msat))
|
assert(invoice.amount_opt === Some(250000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -163,7 +163,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") {
|
test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") {
|
||||||
val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr"
|
val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(250000000 msat))
|
assert(invoice.amount_opt === Some(250000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -178,7 +178,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("Now send $24 for an entire list of things (hashed)") {
|
test("Now send $24 for an entire list of things (hashed)") {
|
||||||
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44"
|
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -193,7 +193,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") {
|
test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") {
|
||||||
val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8"
|
val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lntb")
|
assert(invoice.prefix == "lntb")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -208,7 +208,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") {
|
test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") {
|
||||||
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0"
|
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -227,7 +227,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") {
|
test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") {
|
||||||
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym"
|
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -242,7 +242,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") {
|
test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") {
|
||||||
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse"
|
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -257,7 +257,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") {
|
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") {
|
||||||
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc"
|
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -273,7 +273,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") {
|
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") {
|
||||||
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0"
|
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2000000000 msat))
|
assert(invoice.amount_opt === Some(2000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -298,7 +298,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for (ref <- refs) {
|
for (ref <- refs) {
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix === "lnbc")
|
assert(invoice.prefix === "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2500000000L msat))
|
assert(invoice.amount_opt === Some(2500000000L msat))
|
||||||
assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -317,7 +317,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
|
test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
|
||||||
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372"
|
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix === "lnbc")
|
assert(invoice.prefix === "lnbc")
|
||||||
assert(invoice.amount_opt === Some(2500000000L msat))
|
assert(invoice.amount_opt === Some(2500000000L msat))
|
||||||
assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -336,7 +336,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") {
|
test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") {
|
||||||
val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz"
|
val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix === "lnbc")
|
assert(invoice.prefix === "lnbc")
|
||||||
assert(invoice.amount_opt === Some(967878534 msat))
|
assert(invoice.amount_opt === Some(967878534 msat))
|
||||||
assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f")
|
assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f")
|
||||||
@ -354,7 +354,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") {
|
test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") {
|
||||||
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc"
|
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.prefix == "lnbc")
|
assert(invoice.prefix == "lnbc")
|
||||||
assert(invoice.amount_opt === Some(1000000000 msat))
|
assert(invoice.amount_opt === Some(1000000000 msat))
|
||||||
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
@ -386,7 +386,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x"
|
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x"
|
||||||
)
|
)
|
||||||
for (ref <- refs) {
|
for (ref <- refs) {
|
||||||
assertThrows[Exception](Bolt11Invoice.fromString(ref))
|
assert(Bolt11Invoice.fromString(ref).isFailure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +403,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec)
|
val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec)
|
||||||
assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18))
|
assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18))
|
||||||
val serialized = invoice.toString
|
val serialized = invoice.toString
|
||||||
val pr1 = Bolt11Invoice.fromString(serialized)
|
val Success(pr1) = Bolt11Invoice.fromString(serialized)
|
||||||
assert(invoice == pr1)
|
assert(invoice == pr1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,7 +421,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
signature = ByteVector.empty).sign(priv)
|
signature = ByteVector.empty).sign(priv)
|
||||||
|
|
||||||
val serialized = invoice.toString
|
val serialized = invoice.toString
|
||||||
val pr1 = Bolt11Invoice.fromString(serialized)
|
val Success(pr1) = Bolt11Invoice.fromString(serialized)
|
||||||
val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u }
|
val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,12 +451,12 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
test("accept uppercase invoices") {
|
test("accept uppercase invoices") {
|
||||||
val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j"
|
val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j"
|
||||||
assert(Bolt11Invoice.fromString(input.toUpperCase()).toString == input)
|
assert(Bolt11Invoice.fromString(input.toUpperCase()).get.toString == input)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Pay 1 BTC without multiplier") {
|
test("Pay 1 BTC without multiplier") {
|
||||||
val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58"
|
val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58"
|
||||||
val invoice = Bolt11Invoice.fromString(ref)
|
val Success(invoice) = Bolt11Invoice.fromString(ref)
|
||||||
assert(invoice.amount_opt === Some(100000000000L msat))
|
assert(invoice.amount_opt === Some(100000000000L msat))
|
||||||
assert(features2bits(invoice.features) === BitVector.empty)
|
assert(features2bits(invoice.features) === BitVector.empty)
|
||||||
}
|
}
|
||||||
@ -487,7 +487,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
for ((features, res) <- featureBits) {
|
for ((features, res) <- featureBits) {
|
||||||
val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features)
|
val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features)
|
||||||
assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res)
|
assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res)
|
||||||
assert(Bolt11Invoice.fromString(invoice.toString) === invoice)
|
assert(Bolt11Invoice.fromString(invoice.toString).get === invoice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,17 +514,15 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory))
|
assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory))
|
||||||
assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory)))
|
assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory)))
|
||||||
|
|
||||||
val pr1 = Bolt11Invoice.fromString(invoice.toString)
|
val Success(pr1) = Bolt11Invoice.fromString(invoice.toString)
|
||||||
assert(pr1.paymentSecret === invoice.paymentSecret)
|
assert(pr1.paymentSecret === invoice.paymentSecret)
|
||||||
|
|
||||||
val pr2 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")
|
val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")
|
||||||
assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory)))
|
assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory)))
|
||||||
assert(pr2.paymentSecret === None)
|
assert(pr2.paymentSecret === None)
|
||||||
|
|
||||||
// An invoice that sets the payment secret feature bit must provide a payment secret.
|
// An invoice that sets the payment secret feature bit must provide a payment secret.
|
||||||
assertThrows[IllegalArgumentException](
|
assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure)
|
||||||
Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7")
|
|
||||||
)
|
|
||||||
|
|
||||||
// A multi-part invoice must use a payment secret.
|
// A multi-part invoice must use a payment secret.
|
||||||
assertThrows[IllegalArgumentException](
|
assertThrows[IllegalArgumentException](
|
||||||
@ -544,7 +542,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
assert(pr2.features.hasFeature(BasicMultiPartPayment))
|
assert(pr2.features.hasFeature(BasicMultiPartPayment))
|
||||||
assert(pr2.features.hasFeature(TrampolinePaymentPrototype))
|
assert(pr2.features.hasFeature(TrampolinePaymentPrototype))
|
||||||
|
|
||||||
val pr3 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")
|
val Success(pr3) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")
|
||||||
assert(!pr3.features.hasFeature(TrampolinePaymentPrototype))
|
assert(!pr3.features.hasFeature(TrampolinePaymentPrototype))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,16 +614,17 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for ((req, nodeId) <- requests) {
|
for ((req, nodeId) <- requests) {
|
||||||
assert(Bolt11Invoice.fromString(req).nodeId === nodeId)
|
val Success(invoice) = Bolt11Invoice.fromString(req)
|
||||||
assert(Bolt11Invoice.fromString(req).toString === req)
|
assert(invoice.nodeId === nodeId)
|
||||||
|
assert(invoice.toString === req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("no unknown feature in invoice"){
|
test("no unknown feature in invoice") {
|
||||||
assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty)
|
assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty)
|
||||||
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures())
|
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures())
|
||||||
assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))
|
assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))
|
||||||
assert(Bolt11Invoice.fromString(invoice.toString) === invoice)
|
assert(Bolt11Invoice.fromString(invoice.toString).get === invoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Invoices can't have high features"){
|
test("Invoices can't have high features"){
|
||||||
|
@ -0,0 +1,349 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.eclair.payment
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
|
||||||
|
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto}
|
||||||
|
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||||
|
import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion}
|
||||||
|
import fr.acinq.eclair.payment.Bolt12Invoice.signatureTag
|
||||||
|
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec}
|
||||||
|
import fr.acinq.eclair.wire.protocol.Offers._
|
||||||
|
import fr.acinq.eclair.wire.protocol.{GenericTlv, Offers, TlvStream}
|
||||||
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64, randomKey}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import scodec.bits._
|
||||||
|
|
||||||
|
import scala.concurrent.duration.DurationInt
|
||||||
|
import scala.util.Success
|
||||||
|
|
||||||
|
class Bolt12InvoiceSpec extends AnyFunSuite {
|
||||||
|
|
||||||
|
def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = {
|
||||||
|
val tlvs = Offers.removeSignature(invoice.records)
|
||||||
|
val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key)
|
||||||
|
val signedInvoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None)
|
||||||
|
assert(signedInvoice.checkSignature())
|
||||||
|
signedInvoice
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check invoice signature") {
|
||||||
|
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
|
||||||
|
val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
|
||||||
|
val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain)
|
||||||
|
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty)
|
||||||
|
assert(invoice.isValidFor(offer, request))
|
||||||
|
assert(invoice.checkSignature())
|
||||||
|
assert(!invoice.checkRefundSignature())
|
||||||
|
assert(Bolt12Invoice.fromString(invoice.toString).get.toString === invoice.toString)
|
||||||
|
// changing signature makes check fail
|
||||||
|
val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown), None)
|
||||||
|
assert(!withInvalidSignature.checkSignature())
|
||||||
|
assert(!withInvalidSignature.isValidFor(offer, request))
|
||||||
|
assert(!withInvalidSignature.checkRefundSignature())
|
||||||
|
// changing fields makes the signature invalid
|
||||||
|
val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Seq(GenericTlv(UInt64(7), hex"ade4"))), None)
|
||||||
|
assert(!withModifiedUnknownTlv.checkSignature())
|
||||||
|
assert(!withModifiedUnknownTlv.isValidFor(offer, request))
|
||||||
|
assert(!withModifiedUnknownTlv.checkRefundSignature())
|
||||||
|
val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(amount) => Amount(amount + 100.msat) case x => x }, invoice.records.unknown), None)
|
||||||
|
assert(!withModifiedAmount.checkSignature())
|
||||||
|
assert(!withModifiedAmount.isValidFor(offer, request))
|
||||||
|
assert(!withModifiedAmount.checkRefundSignature())
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice matches offer") {
|
||||||
|
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
|
||||||
|
val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
|
||||||
|
val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain)
|
||||||
|
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty)
|
||||||
|
assert(invoice.isValidFor(offer, request))
|
||||||
|
assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request))
|
||||||
|
// amount must match the offer
|
||||||
|
val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(9000 msat) case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withOtherAmount.isValidFor(offer, request))
|
||||||
|
// description must match the offer, may have appended info
|
||||||
|
val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("other description") case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withOtherDescription.isValidFor(offer, request))
|
||||||
|
val withExtendedDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("test offer + more") case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(withExtendedDescription.isValidFor(offer, request))
|
||||||
|
// nodeId must match the offer
|
||||||
|
val otherNodeKey = randomKey()
|
||||||
|
val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeId(_) => NodeId(otherNodeKey.publicKey) case x => x }.toSeq), None), otherNodeKey)
|
||||||
|
assert(!withOtherNodeId.isValidFor(offer, request))
|
||||||
|
// offerId must match the offer
|
||||||
|
val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withOtherOfferId.isValidFor(offer, request))
|
||||||
|
// issuer must match the offer
|
||||||
|
val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob"))), None), nodeKey)
|
||||||
|
assert(!withOtherIssuer.isValidFor(offer, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice matches invoice request") {
|
||||||
|
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
|
||||||
|
val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain)
|
||||||
|
val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain)
|
||||||
|
assert(request.quantity_opt === None) // when paying for a single item, the quantity field must not be present
|
||||||
|
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional))
|
||||||
|
assert(invoice.isValidFor(offer, request))
|
||||||
|
val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withInvalidFeatures.isValidFor(offer, request))
|
||||||
|
val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withAmountTooBig.isValidFor(offer, request))
|
||||||
|
val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2)), None), nodeKey)
|
||||||
|
assert(!withQuantity.isValidFor(offer, request))
|
||||||
|
val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withOtherPayerKey.isValidFor(offer, request))
|
||||||
|
val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman")), None), nodeKey)
|
||||||
|
assert(!withPayerNote.isValidFor(offer, request))
|
||||||
|
val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506")), None), nodeKey)
|
||||||
|
assert(!withPayerInfo.isValidFor(offer, request))
|
||||||
|
// Invoice request with more details about the payer.
|
||||||
|
val requestWithPayerDetails = {
|
||||||
|
val tlvs: Seq[InvoiceRequestTlv] = Seq(
|
||||||
|
OfferId(offer.offerId),
|
||||||
|
Amount(15000 msat),
|
||||||
|
PayerKey(payerKey.publicKey),
|
||||||
|
PayerInfo(hex"010203040506"),
|
||||||
|
PayerNote("I am Batman"),
|
||||||
|
FeaturesTlv(Features(VariableLengthOnion -> Mandatory))
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey)
|
||||||
|
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
|
||||||
|
}
|
||||||
|
val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, Features.empty)
|
||||||
|
assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails))
|
||||||
|
assert(!withPayerDetails.isValidFor(offer, request))
|
||||||
|
val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withOtherPayerInfo.isValidFor(offer, requestWithPayerDetails))
|
||||||
|
assert(!withOtherPayerInfo.isValidFor(offer, request))
|
||||||
|
val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerNote(_) => PayerNote("Or am I Bruce Wayne?") case x => x }.toSeq), None), nodeKey)
|
||||||
|
assert(!withOtherPayerNote.isValidFor(offer, requestWithPayerDetails))
|
||||||
|
assert(!withOtherPayerNote.isValidFor(offer, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check invoice expiry") {
|
||||||
|
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
|
||||||
|
val offer = Offer(Some(5000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
|
||||||
|
val request = InvoiceRequest(offer, 5000 msat, 1, Features.empty, payerKey, chain)
|
||||||
|
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty)
|
||||||
|
assert(!invoice.isExpired())
|
||||||
|
assert(invoice.isValidFor(offer, request))
|
||||||
|
val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x }), None), nodeKey)
|
||||||
|
assert(expiredInvoice1.isExpired())
|
||||||
|
assert(!expiredInvoice1.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well
|
||||||
|
val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(TimestampSecond.now() - 2000) case x => x } ++ Seq(RelativeExpiry(1800))), None), nodeKey)
|
||||||
|
assert(expiredInvoice2.isExpired())
|
||||||
|
assert(!expiredInvoice2.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check chain compatibility") {
|
||||||
|
val amount = 5000 msat
|
||||||
|
val (nodeKey, payerKey) = (randomKey(), randomKey())
|
||||||
|
val (chain1, chain2) = (randomBytes32(), randomBytes32())
|
||||||
|
val offerBtc = Offer(Some(amount), "bitcoin offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||||
|
val requestBtc = InvoiceRequest(offerBtc, amount, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
val invoiceImplicitBtc = {
|
||||||
|
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||||
|
CreatedAt(TimestampSecond.now()),
|
||||||
|
PaymentHash(Crypto.sha256(randomBytes32())),
|
||||||
|
OfferId(offerBtc.offerId),
|
||||||
|
NodeId(nodeKey.publicKey),
|
||||||
|
Amount(amount),
|
||||||
|
Description(offerBtc.description),
|
||||||
|
PayerKey(payerKey.publicKey)
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||||
|
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
|
||||||
|
}
|
||||||
|
assert(invoiceImplicitBtc.isValidFor(offerBtc, requestBtc))
|
||||||
|
val invoiceExplicitBtc = {
|
||||||
|
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||||
|
Chain(Block.LivenetGenesisBlock.hash),
|
||||||
|
CreatedAt(TimestampSecond.now()),
|
||||||
|
PaymentHash(Crypto.sha256(randomBytes32())),
|
||||||
|
OfferId(offerBtc.offerId),
|
||||||
|
NodeId(nodeKey.publicKey),
|
||||||
|
Amount(amount),
|
||||||
|
Description(offerBtc.description),
|
||||||
|
PayerKey(payerKey.publicKey)
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||||
|
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
|
||||||
|
}
|
||||||
|
assert(invoiceExplicitBtc.isValidFor(offerBtc, requestBtc))
|
||||||
|
val invoiceOtherChain = {
|
||||||
|
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||||
|
Chain(chain1),
|
||||||
|
CreatedAt(TimestampSecond.now()),
|
||||||
|
PaymentHash(Crypto.sha256(randomBytes32())),
|
||||||
|
OfferId(offerBtc.offerId),
|
||||||
|
NodeId(nodeKey.publicKey),
|
||||||
|
Amount(amount),
|
||||||
|
Description(offerBtc.description),
|
||||||
|
PayerKey(payerKey.publicKey)
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||||
|
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
|
||||||
|
}
|
||||||
|
assert(!invoiceOtherChain.isValidFor(offerBtc, requestBtc))
|
||||||
|
val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeId(nodeKey.publicKey))))
|
||||||
|
val requestOtherChains = InvoiceRequest(offerOtherChains, amount, 1, Features.empty, payerKey, chain1)
|
||||||
|
val invoiceOtherChains = {
|
||||||
|
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||||
|
Chain(chain1),
|
||||||
|
CreatedAt(TimestampSecond.now()),
|
||||||
|
PaymentHash(Crypto.sha256(randomBytes32())),
|
||||||
|
OfferId(offerOtherChains.offerId),
|
||||||
|
NodeId(nodeKey.publicKey),
|
||||||
|
Amount(amount),
|
||||||
|
Description(offerOtherChains.description),
|
||||||
|
PayerKey(payerKey.publicKey)
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||||
|
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
|
||||||
|
}
|
||||||
|
assert(invoiceOtherChains.isValidFor(offerOtherChains, requestOtherChains))
|
||||||
|
val invoiceInvalidOtherChain = {
|
||||||
|
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||||
|
Chain(chain2),
|
||||||
|
CreatedAt(TimestampSecond.now()),
|
||||||
|
PaymentHash(Crypto.sha256(randomBytes32())),
|
||||||
|
OfferId(offerOtherChains.offerId),
|
||||||
|
NodeId(nodeKey.publicKey),
|
||||||
|
Amount(amount),
|
||||||
|
Description(offerOtherChains.description),
|
||||||
|
PayerKey(payerKey.publicKey)
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||||
|
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
|
||||||
|
}
|
||||||
|
assert(!invoiceInvalidOtherChain.isValidFor(offerOtherChains, requestOtherChains))
|
||||||
|
val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true }), None), nodeKey)
|
||||||
|
assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("decode invoice") {
|
||||||
|
val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05")
|
||||||
|
val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a")
|
||||||
|
val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly")
|
||||||
|
assert(offer.amount === Some(100_000 msat))
|
||||||
|
assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey))
|
||||||
|
assert(offer.chains === Seq(Block.TestnetGenesisBlock.hash))
|
||||||
|
val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash)
|
||||||
|
val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqq83qk6msaxhyk0n9xnajs5swehp24wndvvn0ftppu7363evzc9uwrnujvg95tuyy05nqkcdetsaljgq4u6789jllc54qrpjrzzn3c38dj3tscu5qgcs2y4lj5gqlvq50uu7sce478j3j0l599nxfs6svx2cfefgn4a0675893wtzuckqfwlcrcq0qspa9zynlpdk9zzechehkemgaksklylxhr7yfjfx6h696th327nm4nsf52xzq0ukchx69g00c4vvk6kzc5jyklneyy05l9tef7a5jcjn5")
|
||||||
|
assert(!invoice.isExpired())
|
||||||
|
assert(invoice.isValidFor(offer, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("decode invoice with quantity") {
|
||||||
|
val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05")
|
||||||
|
val payerKey = PrivateKey(hex"94c7a21a11efa16c5f73b093dc136d9525e2ff40ea7a958c43c1f6004bf6a676")
|
||||||
|
val Success(offer) = Offer.decode("lno1pqpzwyq2pf382mrtyphkven9wgtqzqgcqy9pug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly")
|
||||||
|
assert(offer.amount === Some(10_000 msat))
|
||||||
|
assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey))
|
||||||
|
assert(offer.chains === Seq(Block.LivenetGenesisBlock.hash))
|
||||||
|
val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
val Success(invoice) = Bolt12Invoice.fromString("lni1qss8u47nw2lsgml7fy4jaqwph9f8cl83zfrrhxccvh6076avqzzzv4qgqtp4qzs2vf6kc6eqdanxvetjpsqpug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulysqzpfxyrat02l8wtgtwuc4h5hw6dxhn0hcpdrtu3dpejfjdlw9h4j3nppxc2qyvg9z0lf2yq7wl9ygd6td4cj7whp3ye4cfxrtu7zq4r2mc0mcdspk3duzv7d0stqyh0upuq8sgr44r7aaluwqfw8pkd9f3cgk7ae2l8rkexznhegr0p7w4mlhvfkvlnr5k2lnw0hhsf6ckys3sst7kng5p7m2pxlvdxl3tan809vkk75j")
|
||||||
|
assert(!invoice.isExpired())
|
||||||
|
assert(invoice.amount === 50_000.msat)
|
||||||
|
assert(invoice.quantity === Some(5))
|
||||||
|
assert(invoice.isValidFor(offer, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("decode invalid invoice") {
|
||||||
|
val testCases = Seq(
|
||||||
|
// Missing amount.
|
||||||
|
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3c2pf6x2um5yphkven9wgxqq83q2hfdphr3r8x07ej0z0swprnll58z4jlw36wye7kw63ssm95ru8qjvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncyqyrhddpcla8pprdxwdkmjmutya0kvnrpeqjqa75sr02wff0le52ydckr8ww09gg4jyvxyma903fhh6v8t4edftg7vw6qz7h0tz20p5wq",
|
||||||
|
// Missing node id.
|
||||||
|
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqzvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncypc7vg95clf4z8fklzua72nhavtjp5qp0u7pemgpecypn6q809qr5733c9y0thf6fdnegxleeupddgzgsyszrktay6cjv9cld3wzlw5pq",
|
||||||
|
// Missing payment hash.
|
||||||
|
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32y0cyqhfql7g2dp8w0s5xc0ccgelq4hrnkgmxxltvdq95rqzpf4e68j60h6dysm3evhnu4rwtrqp3dnekmk9sxklw267axtj6zangxtnfx2pq",
|
||||||
|
// Missing description.
|
||||||
|
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcsrqqrcs9t5ksm3c3nn8lve838c8q3ell6r32e0hga8zvlt8dgcgdj6p7rsfxyrf2hn257kgdlwwv42lmlu50yf59paz59ql58rdnnds7808dejt662qyvg9z5ge2yrkqenqxf38w8cyqv98mkllnpp6sfs783yvax3ensc5wlm4n5a9wfuzqjraxg7gnskte8m9lzn2j5r55a4n3nhhfnflzd5953tau0h2auztf9und4psz6p34wx4vsjxwyvc33lezqvm208ntdczqneylzznt0cg",
|
||||||
|
// Missing creation date.
|
||||||
|
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9gswcrxvqexyaclqsps5lwml7vy82pxrc7y3n568xwrz3mlwkwn54e8sgp22vcpqylq4zcxep5faeysey8ucu6wrfgffphlt457rd9f7tlpnyltlqz0yd9eqjcehyu3zwvear2e4ksx32t3qtek9gucrephdmsk0",
|
||||||
|
// Missing signature.
|
||||||
|
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32yv4zpmqvesrycnhruzqxznam0lessagyc0rcjxwnguecv280a6e6wjhy",
|
||||||
|
)
|
||||||
|
for (testCase <- testCases) {
|
||||||
|
assert(Bolt12Invoice.fromString(testCase).isFailure, testCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("encode/decode invoice with many fields") {
|
||||||
|
val chain = Block.TestnetGenesisBlock.hash
|
||||||
|
val offerId = ByteVector32.fromValidHex("8bc5978de5d625c90136dfa896a8a02cef33c5457027684687e3f98e0cfca4f0")
|
||||||
|
val amount = 123456 msat
|
||||||
|
val description = "invoice with many fields"
|
||||||
|
val features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||||
|
val issuer = "acinq.co"
|
||||||
|
val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90")
|
||||||
|
val quantity = 57
|
||||||
|
val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65")
|
||||||
|
val payerNote = "I'm the king"
|
||||||
|
val payerInfo = hex"a9eb6e526eac59cd9b89fb20"
|
||||||
|
val createdAt = TimestampSecond(1654654654L)
|
||||||
|
val paymentHash = ByteVector32.fromValidHex("51951d4c53c904035f0b293dc9df1c0e7967213430ae07a5f3e134cd33325341")
|
||||||
|
val relativeExpiry = 3600
|
||||||
|
val cltv = CltvExpiryDelta(123)
|
||||||
|
val fallbacks = Seq(FallbackAddress(4, hex"123d56f8"), FallbackAddress(6, hex"eb3adc68945ef601"))
|
||||||
|
val replaceInvoice = ByteVector32.fromValidHex("71ad033e5f42068225608770fa7672505449425db543a1f9c23bf03657aa37c1")
|
||||||
|
val tlvs = TlvStream[InvoiceTlv](Seq(
|
||||||
|
Chain(chain),
|
||||||
|
OfferId(offerId),
|
||||||
|
Amount(amount),
|
||||||
|
Description(description),
|
||||||
|
FeaturesTlv(features),
|
||||||
|
Issuer(issuer),
|
||||||
|
NodeId(nodeKey.publicKey),
|
||||||
|
Quantity(quantity),
|
||||||
|
PayerKey(payerKey),
|
||||||
|
PayerNote(payerNote),
|
||||||
|
PayerInfo(payerInfo),
|
||||||
|
CreatedAt(createdAt),
|
||||||
|
PaymentHash(paymentHash),
|
||||||
|
RelativeExpiry(relativeExpiry),
|
||||||
|
Cltv(cltv),
|
||||||
|
Fallbacks(fallbacks),
|
||||||
|
ReplaceInvoice(replaceInvoice)
|
||||||
|
), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex"")))
|
||||||
|
val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey)
|
||||||
|
val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None)
|
||||||
|
assert(invoice.toString === "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsq9qgv93kjmn39e3k783qc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8p3yjwmfqw35x2grtd9hxw2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq6knwqxzve9r5kpufq3szuhn9f437cj05az5kqnsl9wefhfnwzenf5z68qh5jj48rmku97u0gzdm2wlkuwrylpvqfttdtw972cwdteal6qfhqvqsyqlaqyusq")
|
||||||
|
val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString)
|
||||||
|
assert(codedDecoded.chain === chain)
|
||||||
|
assert(codedDecoded.offerId === Some(offerId))
|
||||||
|
assert(codedDecoded.amount === amount)
|
||||||
|
assert(codedDecoded.description === Left(description))
|
||||||
|
assert(codedDecoded.features === features)
|
||||||
|
assert(codedDecoded.issuer === Some(issuer))
|
||||||
|
assert(codedDecoded.nodeId.value.drop(1) === nodeKey.publicKey.value.drop(1))
|
||||||
|
assert(codedDecoded.quantity === Some(quantity))
|
||||||
|
assert(codedDecoded.payerKey === Some(payerKey))
|
||||||
|
assert(codedDecoded.payerNote === Some(payerNote))
|
||||||
|
assert(codedDecoded.payerInfo === Some(payerInfo))
|
||||||
|
assert(codedDecoded.createdAt === createdAt)
|
||||||
|
assert(codedDecoded.paymentHash === paymentHash)
|
||||||
|
assert(codedDecoded.relativeExpiry === relativeExpiry.seconds)
|
||||||
|
assert(codedDecoded.minFinalCltvExpiryDelta === cltv)
|
||||||
|
assert(codedDecoded.fallbacks === Some(fallbacks))
|
||||||
|
assert(codedDecoded.replaceInvoice === Some(replaceInvoice))
|
||||||
|
assert(codedDecoded.records.unknown.toSet === Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex"")))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,283 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.eclair.wire.protocol
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
|
||||||
|
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32}
|
||||||
|
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||||
|
import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion}
|
||||||
|
import fr.acinq.eclair.wire.protocol.OfferCodecs.invoiceRequestTlvCodec
|
||||||
|
import fr.acinq.eclair.wire.protocol.Offers._
|
||||||
|
import fr.acinq.eclair.{Features, MilliSatoshiLong, randomBytes32, randomKey}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import scodec.bits.{ByteVector, HexStringSyntax}
|
||||||
|
|
||||||
|
import scala.util.Success
|
||||||
|
|
||||||
|
class OffersSpec extends AnyFunSuite {
|
||||||
|
val nodeId = ByteVector32(hex"4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605")
|
||||||
|
|
||||||
|
test("sign and check offer") {
|
||||||
|
val key = randomKey()
|
||||||
|
val offer = Offer(Some(100_000 msat), "test offer", key.publicKey, Features(VariableLengthOnion -> Mandatory), Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(offer.signature.isEmpty)
|
||||||
|
val signedOffer = offer.sign(key)
|
||||||
|
assert(signedOffer.checkSignature())
|
||||||
|
}
|
||||||
|
|
||||||
|
test("invoice request is signed") {
|
||||||
|
val sellerKey = randomKey()
|
||||||
|
val offer = Offer(Some(100_000 msat), "test offer", sellerKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(request.checkSignature())
|
||||||
|
}
|
||||||
|
|
||||||
|
test("basic offer") {
|
||||||
|
val encoded = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczs"
|
||||||
|
val offer = Offer.decode(encoded).get
|
||||||
|
assert(offer.amount.isEmpty)
|
||||||
|
assert(offer.signature.isEmpty)
|
||||||
|
assert(offer.description === "Offer by rusty's node")
|
||||||
|
assert(offer.nodeIdXOnly === nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("basic signed offer") {
|
||||||
|
val encodedSigned = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqs85ck65ycmkdk92smwt9zuewdzfe7v4aavvaz5kgv9mkk63v3s0ge0f099kssh3yc95qztx504hu92hnx8ctzhtt08pgk0texz0509tk"
|
||||||
|
val Success(signedOffer) = Offer.decode(encodedSigned)
|
||||||
|
assert(signedOffer.checkSignature())
|
||||||
|
assert(signedOffer.amount.isEmpty)
|
||||||
|
assert(signedOffer.description === "Offer by rusty's node")
|
||||||
|
assert(signedOffer.nodeIdXOnly === nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("offer with amount and quantity") {
|
||||||
|
val encoded = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykq5"
|
||||||
|
val Success(offer) = Offer.decode(encoded)
|
||||||
|
assert(offer.amount === Some(50 msat))
|
||||||
|
assert(offer.signature.isEmpty)
|
||||||
|
assert(offer.description === "50msat multi-quantity offer")
|
||||||
|
assert(offer.nodeIdXOnly === nodeId)
|
||||||
|
assert(offer.issuer === Some("rustcorp.com.au"))
|
||||||
|
assert(offer.quantityMin === Some(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("signed offer with amount and quantity") {
|
||||||
|
val encodedSigned = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykqhcypjju7unu05vav8yvhn27lztf46k9gqlga8uvu4uq62kpuywnu6me8srgh2q7puczukr8arectaapfl5d4rd6uc7st7tnqf0ttx39n40s"
|
||||||
|
val Success(signedOffer) = Offer.decode(encodedSigned)
|
||||||
|
assert(signedOffer.checkSignature())
|
||||||
|
assert(signedOffer.amount === Some(50 msat))
|
||||||
|
assert(signedOffer.description === "50msat multi-quantity offer")
|
||||||
|
assert(signedOffer.nodeIdXOnly === nodeId)
|
||||||
|
assert(signedOffer.issuer === Some("rustcorp.com.au"))
|
||||||
|
assert(signedOffer.quantityMin === Some(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("decode invalid offer") {
|
||||||
|
val testCases = Seq(
|
||||||
|
"lno1pgxx7enxv4e8xgrjda3kkgg", // missing node id
|
||||||
|
"lno1rcsdhss957tylk58rmly849jupnmzs52ydhxhl8fgz7994xkf2hnwhg", // missing description
|
||||||
|
)
|
||||||
|
for (testCase <- testCases) {
|
||||||
|
assert(Offer.decode(testCase).isFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest = {
|
||||||
|
val tlvs = removeSignature(request.records)
|
||||||
|
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(tlvs, invoiceRequestTlvCodec), key)
|
||||||
|
val signedRequest = InvoiceRequest(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))))
|
||||||
|
assert(signedRequest.checkSignature())
|
||||||
|
signedRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice request matches offer") {
|
||||||
|
val offer = Offer(Some(2500 msat), "basic offer", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(request.isValidFor(offer))
|
||||||
|
val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(3000 msat) case x => x })), payerKey)
|
||||||
|
assert(biggerAmount.isValidFor(offer))
|
||||||
|
val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(2000 msat) case x => x })), payerKey)
|
||||||
|
assert(!lowerAmount.isValidFor(offer))
|
||||||
|
val otherOfferId = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x })), payerKey)
|
||||||
|
assert(!otherOfferId.isValidFor(offer))
|
||||||
|
val withQuantity = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Quantity(1)))), payerKey)
|
||||||
|
assert(!withQuantity.isValidFor(offer))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice request matches offer (with features)") {
|
||||||
|
val offer = Offer(Some(2500 msat), "offer with features", randomKey().publicKey, Features(VariableLengthOnion -> Optional), Block.LivenetGenesisBlock.hash)
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(request.isValidFor(offer))
|
||||||
|
val withoutFeatures = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(withoutFeatures.isValidFor(offer))
|
||||||
|
val otherFeatures = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory), payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(!otherFeatures.isValidFor(offer))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice request matches offer (without amount)") {
|
||||||
|
val offer = Offer(None, "offer without amount", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request = InvoiceRequest(offer, 500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(request.isValidFor(offer))
|
||||||
|
val withoutAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.filter { case Amount(_) => false case _ => true })), payerKey)
|
||||||
|
assert(!withoutAmount.isValidFor(offer))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice request matches offer (chain compatibility)") {
|
||||||
|
{
|
||||||
|
val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeId(randomKey().publicKey))))
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request = {
|
||||||
|
val tlvs: Seq[InvoiceRequestTlv] = Seq(
|
||||||
|
OfferId(offer.offerId),
|
||||||
|
Amount(100 msat),
|
||||||
|
PayerKey(payerKey.publicKey),
|
||||||
|
FeaturesTlv(Features.empty)
|
||||||
|
)
|
||||||
|
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey)
|
||||||
|
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
|
||||||
|
}
|
||||||
|
assert(request.isValidFor(offer))
|
||||||
|
val withDefaultChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.LivenetGenesisBlock.hash)))), payerKey)
|
||||||
|
assert(withDefaultChain.isValidFor(offer))
|
||||||
|
val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.TestnetGenesisBlock.hash)))), payerKey)
|
||||||
|
assert(!otherChain.isValidFor(offer))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
val (chain1, chain2) = (randomBytes32(), randomBytes32())
|
||||||
|
val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeId(randomKey().publicKey))))
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1)
|
||||||
|
assert(request1.isValidFor(offer))
|
||||||
|
val request2 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain2)
|
||||||
|
assert(request2.isValidFor(offer))
|
||||||
|
val noChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.filter { case Chain(_) => false case _ => true })), payerKey)
|
||||||
|
assert(!noChain.isValidFor(offer))
|
||||||
|
val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { case Chain(_) => Chain(Block.LivenetGenesisBlock.hash) case x => x })), payerKey)
|
||||||
|
assert(!otherChain.isValidFor(offer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("check that invoice request matches offer (multiple items)") {
|
||||||
|
val offer = Offer(TlvStream(
|
||||||
|
Amount(500 msat),
|
||||||
|
Description("offer for multiple items"),
|
||||||
|
NodeId(randomKey().publicKey),
|
||||||
|
QuantityMin(3),
|
||||||
|
QuantityMax(10),
|
||||||
|
))
|
||||||
|
val payerKey = randomKey()
|
||||||
|
val request = InvoiceRequest(offer, 1600 msat, 3, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(request.records.get[Quantity].nonEmpty)
|
||||||
|
assert(request.isValidFor(offer))
|
||||||
|
val invalidAmount = InvoiceRequest(offer, 2400 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(!invalidAmount.isValidFor(offer))
|
||||||
|
val tooFewItems = InvoiceRequest(offer, 1000 msat, 2, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(!tooFewItems.isValidFor(offer))
|
||||||
|
val tooManyItems = InvoiceRequest(offer, 5500 msat, 11, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(!tooManyItems.isValidFor(offer))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("decode invoice request") {
|
||||||
|
val encoded = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypz8xu3xwsqpar9dd26lgrrvc7s63ljt0pgh6ag2utv5udez7n2mjzqzz47qcqczqgqzqqgzycsv2tmjgzc5l546aldq699wj9pdusvfred97l352p4aa862vqvzw5p8pdyjqctdyppxzardv9hrypx74klwluzqd0rqgeew2uhuagttuv6aqwklvm0xmlg52lfnagzw8ygt0wrtnv2tsx69m6tgug7njaw5ypa5fn369n9yzc87v02rqccj9h04dxf3nzc"
|
||||||
|
val Success(request) = InvoiceRequest.decode(encoded)
|
||||||
|
assert(request.amount === Some(5500 msat))
|
||||||
|
assert(request.offerId === ByteVector32(hex"4473722674001e8cad6ab5f40c6cc7a1a8fe4b78517d750ae2d94e3722f4d5b9"))
|
||||||
|
assert(request.quantity === 2)
|
||||||
|
assert(request.features === Features(VariableLengthOnion -> Optional, BasicMultiPartPayment -> Optional))
|
||||||
|
assert(request.records.get[Chain].nonEmpty)
|
||||||
|
assert(request.chain === Block.LivenetGenesisBlock.hash)
|
||||||
|
assert(request.payerKey === ByteVector32(hex"c52f7240b14fd2baefda0d14ae9142de41891e5a5f7e34506bde9f4a60182750"))
|
||||||
|
assert(request.payerInfo === Some(hex"deadbeef"))
|
||||||
|
assert(request.payerNote === Some("I am Batman"))
|
||||||
|
assert(request.encode() === encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("decode invalid invoice request") {
|
||||||
|
val testCases = Seq(
|
||||||
|
// Missing offer id.
|
||||||
|
"lnr1pqpp8zqvqqnzqq7pw52tqj6pj2mar5cgkmnt9xe3tj40nxc3pp95xml2e8v432ny7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g",
|
||||||
|
// Missing payer key.
|
||||||
|
"lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqq7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g",
|
||||||
|
// Missing signature.
|
||||||
|
"lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqqycsq8st4zjcyksvjklgaxz9ku6efkv2u4tuekyggfdpkl6kfm9v25eq",
|
||||||
|
)
|
||||||
|
for (testCase <- testCases) {
|
||||||
|
assert(InvoiceRequest.decode(testCase).isFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("compute merkle tree root") {
|
||||||
|
import scodec.Codec
|
||||||
|
import scodec.codecs.list
|
||||||
|
|
||||||
|
case class TestCase(tlvs: ByteVector, count: Int, expected: ByteVector32)
|
||||||
|
|
||||||
|
val testCases = Seq(
|
||||||
|
// Official test vectors.
|
||||||
|
TestCase(hex"010203e8", 1, ByteVector32(hex"aa0aa0f694c85492ac459c1de9831a37682985f5e840ecc9b1e28eece7dc5236")),
|
||||||
|
TestCase(hex"010203e8 02080000010000020003", 2, ByteVector32(hex"013b756ed73554cbc4dd3d90f363cb7cba6d8a279465a21c464e582b173ff502")),
|
||||||
|
TestCase(hex"010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", 3, ByteVector32(hex"016fcda3b6f9ca30b35936877ca591fa101365a761a1453cfd9436777d593656")),
|
||||||
|
TestCase(hex"0603555344 080203e8 0a0f313055534420657665727920646179 141072757374792e6f7a6c6162732e6f7267 1a020101 1e204b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605", 6, ByteVector32(hex"7cef68df49fd9222bed0138ca5603da06464b2f523ea773bc4edcb6bd07966e7")),
|
||||||
|
// Additional test vectors.
|
||||||
|
TestCase(hex"010100", 1, ByteVector32(hex"c8112b235945b06a11995bf69956a93ff0403c28de35bd33b4714da1b6239ebb")),
|
||||||
|
TestCase(hex"010100 020100", 2, ByteVector32(hex"8271d606bea3ef49e59d610585317edfc6c53d8d1afd763731919d9a7d70a7d9")),
|
||||||
|
TestCase(hex"010100 020100 030100", 3, ByteVector32(hex"c7eff290817749d87eede061d5335559e8211769e651a2ee5c5e7d2ddd655236")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100", 4, ByteVector32(hex"57883ef2f1e8df4a23e6f0a2e3acda2ed0b11e00ef2d39fe1caa2d71d7273c37")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100", 5, ByteVector32(hex"85b74f254eced46c525a5369c52f86f249a41f6f6ccb3c918ffe4025ea22d8b6")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100", 6, ByteVector32(hex"6cf27da8a67b7cb199dd1824017cb008bd22bf1d57273a8c4544c5408275dc2d")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100", 7, ByteVector32(hex"2a038f022b51b1b969563679a22eb167ef603d5b2cb2d0dbe86dc4d2f48d8c6e")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100", 8, ByteVector32(hex"8ddbe97f6ed2e2a4a43e828e350f9cb6679b7d5f16837273cf0e6f7da342fa19")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100", 9, ByteVector32(hex"8050bed857ff7929a24251e3a517fc14f46fb0f02e6719cb9d53087f7f047f6d")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", 10, ByteVector32(hex"e22aa818e746ff9613e6cccc99ebce257d93c35736b168b6b478d6f3762f56ce")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", 11, ByteVector32(hex"626e3159cec72534155ccf691a84ab68da89e6cd679a118c70a28fd1f1bb10cc")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", 12, ByteVector32(hex"f659da1c839d99a2c6b104d179ee44ffe3a9eaa55831de3c612c8c293c27401b")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", 13, ByteVector32(hex"c165756a94718d19e66ff7b581347699009a9e17805e16cb6ba94c034c7dc757")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", 14, ByteVector32(hex"573b85bbceacbf1b189412858ac6573e923bbf0c9cfdc37d37757996f6086208")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", 15, ByteVector32(hex"84a3088fe74b82ee82a9415d48fdfad8dc6a286fec8e6fcdcefcf0bc02f3256e")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", 16, ByteVector32(hex"a686f116fce33e43fa875fec2253c71694a0324e1ce7640ed1070b0cc3a14cc1")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", 17, ByteVector32(hex"fbee87d6726c8b67a8d2e2bff92b13d0b1d9188f9d42af2d3afefceaafa6f3e5")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", 18, ByteVector32(hex"5004f619c426b01e57c480a84d5dcdc3a70b4bf575ec573be60c3a75ed978b72")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", 19, ByteVector32(hex"6f0a5e59f1fa5dc6c12ed3bbe0eb91c818b22a8d011d5a2160462c59e6158a58")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100", 20, ByteVector32(hex"e43f00c262e4578c5ed4413ab340e79cb8b258241b7c52550b7307f7b9c4d645")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100", 21, ByteVector32(hex"e46776637883bae1a62cbfb621c310c13e6c522092954e08d74c08328d53f035")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100", 22, ByteVector32(hex"813ebe9f07638005abbe270f11ae2749a5b9b0c5cf89a305598303a38f5f2da5")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100", 23, ByteVector32(hex"fdd7b779192dcbadb5695303e2bcee0fc175428278bdbfa4b4445251df6c9450")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100", 24, ByteVector32(hex"33c92b7820742d094548328ee3bfdf29bf3fe1f971171dcd2a6da0f185dceddb")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100", 25, ByteVector32(hex"888da09f5ba1b8e431b3ab1db62fca94c0cbbec6b145012d9308d20f68571ff2")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100", 26, ByteVector32(hex"a87cdc040109b855d81f13af4a6f57cdb7e31252eeb83bc03518fdd6dd81ec18")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100", 27, ByteVector32(hex"9829715a0d8cbb5c080de53704f274aa4da3590e8338d57ce99ab491d7a44e76")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100", 28, ByteVector32(hex"ce8bac7c3d10b528d59d5f391bf36bb6acd65b4bb3cbd0a769488e3b451b2c26")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100", 29, ByteVector32(hex"88d29ac3e4ae8761058af4b1baaa873ec4f76822166f8dfc2888bcbb51212130")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100", 30, ByteVector32(hex"b013259fe32c6eaf88d2b3b2d01350e5505bcc0fcdcdc7c360e5644fe827424d")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100", 31, ByteVector32(hex"1c60489269d312c2ea94c637936e38a968d2900cab6c5544db091aa8b3bb5176")),
|
||||||
|
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100 200100", 32, ByteVector32(hex"e0d88bd7685ffd55e0de4e45e190e7e6bf1ecc0a7d1a32fbdaa6b1b27e8bc37b")),
|
||||||
|
)
|
||||||
|
testCases.foreach {
|
||||||
|
case TestCase(tlvStream, tlvCount, expectedRoot) =>
|
||||||
|
val genericTlvStream: Codec[TlvStream[GenericTlv]] = list(TlvCodecs.genericTlv).xmap(tlvs => TlvStream(tlvs), tlvs => tlvs.records.toList)
|
||||||
|
val tlvs = genericTlvStream.decode(tlvStream.bits).require.value
|
||||||
|
assert(tlvs.records.size === tlvCount)
|
||||||
|
val root = Offers.rootHash(tlvs, genericTlvStream)
|
||||||
|
assert(root === expectedRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -44,7 +44,7 @@ object FormParamExtractors {
|
|||||||
|
|
||||||
implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin))
|
implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin))
|
||||||
|
|
||||||
implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest) }
|
implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest).get }
|
||||||
|
|
||||||
implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) }
|
implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) }
|
||||||
|
|
||||||
|
@ -824,7 +824,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
|
|||||||
|
|
||||||
test("'getreceivedinfo' 2") {
|
test("'getreceivedinfo' 2") {
|
||||||
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
||||||
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
|
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
|
||||||
val eclair = mock[Eclair]
|
val eclair = mock[Eclair]
|
||||||
val pending = randomBytes32()
|
val pending = randomBytes32()
|
||||||
eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment))
|
eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment))
|
||||||
@ -844,7 +844,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
|
|||||||
|
|
||||||
test("'getreceivedinfo' 3") {
|
test("'getreceivedinfo' 3") {
|
||||||
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
||||||
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
|
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
|
||||||
val eclair = mock[Eclair]
|
val eclair = mock[Eclair]
|
||||||
val expired = randomBytes32()
|
val expired = randomBytes32()
|
||||||
eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired)))
|
eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired)))
|
||||||
@ -864,7 +864,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
|
|||||||
|
|
||||||
test("'getreceivedinfo' 4") {
|
test("'getreceivedinfo' 4") {
|
||||||
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
||||||
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
|
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
|
||||||
val eclair = mock[Eclair]
|
val eclair = mock[Eclair]
|
||||||
val received = randomBytes32()
|
val received = randomBytes32()
|
||||||
eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L)))))
|
eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L)))))
|
||||||
@ -989,7 +989,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
|
|||||||
|
|
||||||
test("'findroute' method response should support nodeId, shortChannelId and full formats") {
|
test("'findroute' method response should support nodeId, shortChannelId and full formats") {
|
||||||
val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh"
|
val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh"
|
||||||
val invoice = Invoice.fromString(serializedInvoice)
|
val invoice = Invoice.fromString(serializedInvoice).get
|
||||||
|
|
||||||
val mockChannelUpdate1 = ChannelUpdate(
|
val mockChannelUpdate1 = ChannelUpdate(
|
||||||
signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"),
|
signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"),
|
||||||
|
2
pom.xml
2
pom.xml
@ -72,7 +72,7 @@
|
|||||||
<akka.version>2.6.18</akka.version>
|
<akka.version>2.6.18</akka.version>
|
||||||
<akka.http.version>10.2.7</akka.http.version>
|
<akka.http.version>10.2.7</akka.http.version>
|
||||||
<sttp.version>3.4.1</sttp.version>
|
<sttp.version>3.4.1</sttp.version>
|
||||||
<bitcoinlib.version>0.22</bitcoinlib.version>
|
<bitcoinlib.version>0.23</bitcoinlib.version>
|
||||||
<guava.version>31.1-jre</guava.version>
|
<guava.version>31.1-jre</guava.version>
|
||||||
<kamon.version>2.4.6</kamon.version>
|
<kamon.version>2.4.6</kamon.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
Loading…
Reference in New Issue
Block a user