Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ It can be enabled by setting `eclair.features.option_attributable_failure = opti
### API changes

- `listoffers` now returns more details about each offer.
- 'parseoffer' is added to display offer fields in a human-readable format.
Comment thread
t-bast marked this conversation as resolved.
Outdated


### Configuration changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
import org.json4s
import org.json4s.JsonAST._
import org.json4s.jackson.Serialization
Expand Down Expand Up @@ -476,6 +477,52 @@ object InvoiceSerializer extends MinimalSerializer({
JObject(fieldList)
})

private case class BlindedRouteJson(firstNodeId: EncodedNodeId, length: Int)
private case class OfferJson(chains: Option[Seq[String]],
amount: Option[String],
currency: Option[String],
description: Option[String],
expiry: Option[TimestampSecond],
issuer: Option[String],
nodeId: Option[PublicKey],
paths: Option[Seq[BlindedRouteJson]],
quantityMax: Option[Long],
features: Option[Features[Feature]],
metadata: Option[String],
unknownTlvs: Option[Map[String, String]])
object OfferSerializer extends ConvertClassSerializer[Offer](o => {
val fractionDigits = o.records.get[OfferTypes.OfferCurrency].map(_.currency.getDefaultFractionDigits()).getOrElse(3)
OfferJson(
chains = o.records.get[OfferTypes.OfferChains].map(_.chains.map(_.toString())),
amount = o.records.get[OfferTypes.OfferAmount].map(a =>
if (fractionDigits == 0) {
a.amount.toString
} else {
val one = scala.math.pow(10, fractionDigits).toInt
s"${a.amount / one}.%0${fractionDigits}d".format(a.amount % one)
}
Comment thread
t-bast marked this conversation as resolved.
),
currency = if (o.records.get[OfferTypes.OfferAmount].isEmpty) {
None
} else {
Some(o.records.get[OfferTypes.OfferCurrency].map(_.currency.getCurrencyCode()).getOrElse("satoshi"))
},
description = o.records.get[OfferTypes.OfferDescription].map(_.description),
expiry = o.records.get[OfferTypes.OfferAbsoluteExpiry].map(_.absoluteExpiry),
issuer = o.records.get[OfferTypes.OfferIssuer].map(_.issuer),
nodeId = o.records.get[OfferTypes.OfferNodeId].map(_.publicKey),
paths = o.records.get[OfferTypes.OfferPaths].map(_.paths.map(p => BlindedRouteJson(p.firstNodeId, p.blindedHops.length))),
quantityMax = o.records.get[OfferTypes.OfferQuantityMax].map(_.max),
features = o.records.get[OfferTypes.OfferFeatures].map(_.features),
metadata = o.records.get[OfferTypes.OfferMetadata].map(_.data.toHex),
unknownTlvs = if (o.records.unknown.isEmpty) {
None
} else {
Some(o.records.unknown.map(tlv => tlv.tag.toString -> tlv.value.toHex).toMap)
}
)
})

private case class OfferDataJson(amountMsat: Option[MilliSatoshi],
description: Option[String],
issuer: Option[String],
Expand Down Expand Up @@ -733,6 +780,7 @@ object JsonSerializers {
NodeAddressSerializer +
DirectedHtlcSerializer +
InvoiceSerializer +
OfferSerializer +
OfferDataSerializer +
JavaUUIDSerializer +
OriginSerializer +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private class OfferCreator(context: ActorContext[OfferCreator.Command],
} else {
val tlvs: Set[OfferTlv] = Set(
if (nodeParams.chainHash != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(nodeParams.chainHash))) else None,
amount_opt.map(OfferAmount),
amount_opt.map(_.toLong).map(OfferAmount),
description_opt.map(OfferDescription),
expiry_opt.map(OfferAbsoluteExpiry),
issuer_opt.map(OfferIssuer),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,25 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tmillisatoshi, tu32, tu64overflow}
import fr.acinq.eclair.{EncodedNodeId, TimestampSecond, UInt64}
import scodec.Codec
import scodec.{Attempt, Codec}
import scodec.codecs._

import java.util.Currency
import scala.util.Try

object OfferCodecs {
private val offerChains: Codec[OfferChains] = tlvField(list(blockHash).xmap[Seq[BlockHash]](_.toSeq, _.toList))

private val offerMetadata: Codec[OfferMetadata] = tlvField(bytes)

private val offerCurrency: Codec[OfferCurrency] = tlvField(utf8)
private val offerCurrency: Codec[OfferCurrency] =
tlvField(utf8.narrow[Currency](s => Attempt.fromTry(Try{
val c = Currency.getInstance(s)
require(c.getDefaultFractionDigits() >= 0)
c
}), _.getCurrencyCode()))
Comment thread
t-bast marked this conversation as resolved.
Outdated

private val offerAmount: Codec[OfferAmount] = tlvField(tmillisatoshi)
private val offerAmount: Codec[OfferAmount] = tlvField(tu64overflow)

private val offerDescription: Codec[OfferDescription] = tlvField(utf8)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute
import fr.acinq.eclair.wire.protocol.CommonCodecs.varint
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv
import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64, nodeFee, randomBytes32}
import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Feature, Features, MilliSatoshi, MilliSatoshiLong, TimestampSecond, UInt64, nodeFee, randomBytes32}
import scodec.Codec
import scodec.bits.ByteVector
import scodec.codecs.vector

import java.util.Currency
import scala.util.{Failure, Try}

/**
Expand Down Expand Up @@ -71,12 +72,12 @@ object OfferTypes {
/**
* Three-letter code of the currency the offer is denominated in. If empty, bitcoin is implied.
*/
case class OfferCurrency(iso4217: String) extends OfferTlv
case class OfferCurrency(currency: Currency) extends OfferTlv

/**
* Amount to pay per item. As we only support bitcoin, the amount is in msat.
* Amount to pay per item.
*/
case class OfferAmount(amount: MilliSatoshi) extends OfferTlv
case class OfferAmount(amount: Long) extends OfferTlv

/**
* Description of the purpose of the payment.
Expand Down Expand Up @@ -238,7 +239,7 @@ object OfferTypes {
case class Offer(records: TlvStream[OfferTlv]) {
val chains: Seq[BlockHash] = records.get[OfferChains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash))
val metadata: Option[ByteVector] = records.get[OfferMetadata].map(_.data)
val amount: Option[MilliSatoshi] = records.get[OfferAmount].map(_.amount)
val amount: Option[MilliSatoshi] = if (records.get[OfferCurrency].isEmpty) records.get[OfferAmount].map(_.amount.msat) else None
val description: Option[String] = records.get[OfferDescription].map(_.description)
val features: Features[Bolt12Feature] = records.get[OfferFeatures].map(_.features.bolt12Features()).getOrElse(Features.empty)
val expiry: Option[TimestampSecond] = records.get[OfferAbsoluteExpiry].map(_.absoluteExpiry)
Expand Down Expand Up @@ -279,7 +280,7 @@ object OfferTypes {
require(amount_opt.isEmpty || description_opt.nonEmpty)
val tlvs: Set[OfferTlv] = Set(
if (chain != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(chain))) else None,
amount_opt.map(OfferAmount),
amount_opt.map(_.toLong).map(OfferAmount),
description_opt.map(OfferDescription),
if (!features.isEmpty) Some(OfferFeatures(features.unscoped())) else None,
Some(OfferNodeId(nodeId)),
Expand All @@ -297,7 +298,7 @@ object OfferTypes {
require(amount_opt.isEmpty || description_opt.nonEmpty)
val tlvs: Set[OfferTlv] = Set(
if (chain != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(chain))) else None,
amount_opt.map(OfferAmount),
amount_opt.map(_.toLong).map(OfferAmount),
description_opt.map(OfferDescription),
if (!features.isEmpty) Some(OfferFeatures(features.unscoped())) else None,
Some(OfferPaths(paths))
Expand All @@ -308,8 +309,6 @@ object OfferTypes {
def validate(records: TlvStream[OfferTlv]): Either[InvalidTlvPayload, Offer] = {
if (records.get[OfferDescription].isEmpty && records.get[OfferAmount].nonEmpty) return Left(MissingRequiredTlv(UInt64(10)))
if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(22)))
// Currency conversion isn't supported yet.
if (records.get[OfferCurrency].nonEmpty) return Left(ForbiddenTlv(UInt64(6)))
if (records.unknown.exists(!isOfferTlv(_))) return Left(ForbiddenTlv(records.unknown.find(!isOfferTlv(_)).get.tag))
Right(Offer(records))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
import fr.acinq.eclair.db.OfferData
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.io.Peer.PeerInfo
import fr.acinq.eclair.payment.{Invoice, PaymentSettlingOnChain}
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions.{CommitmentSpec, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
import fr.acinq.eclair.wire.protocol.OfferTypes.{Offer, OfferTlv}
import fr.acinq.eclair.wire.protocol._
import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.matchers.should.Matchers
import scodec.bits._

import java.net.InetAddress
import java.util.UUID
import java.util.{Currency, UUID}

class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Matchers {

Expand Down Expand Up @@ -334,6 +334,30 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"amount":456001234,"nodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","paymentHash":"2cb0e7b052366787450c33daf6d2f2c3cb6132221326e1c1b49ac97fdd7eb720","description":"minimal offer","features":{"activated":{},"unknown":[]},"blindedPaths":[{"introductionNodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","blindedNodeIds":["031fca650042031dcb777156ef66806c73b01a7f52c4e73c89a0d15823a1ac6237"]}],"createdAt":1665412681,"expiresAt":1665412981,"serialized":"lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"}"""
}

test("Bolt 12 offer") {
val minimalOffer = Offer(TlvStream[OfferTlv](OfferTypes.OfferNodeId(PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"))))
JsonSerializers.serialization.write(minimalOffer)(JsonSerializers.formats) shouldBe """{"nodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"}"""

val ref = "lno1pqzqzltcgq9q6urvv4shxefqv3hkuct5v58qxrp4qqfquctvd93k2srpvd5kuufwvdh3vggzg2hd49ueds8phzcahvh4p2m3pnen649dza2h3k6gxpaequr8fhtq"
val offer = Offer.decode(ref).get
JsonSerializers.serialization.write(offer)(JsonSerializers.formats) shouldBe """{"amount":"25000.000","currency":"satoshi","description":"please donate","expiry":{"iso":"1970-01-10T06:13:20Z","unix":800000},"issuer":"alice@acinq.co","nodeId":"0242aeda97996c0e1b8b1dbb2f50ab710cf33d54ad175578db48307b9070674dd6"}"""

val bigOffer = Offer(TlvStream(Set[OfferTlv](
OfferTypes.OfferChains(Seq(Block.Testnet4GenesisBlock.hash)),
OfferTypes.OfferMetadata(hex"d5f4a6"),
OfferTypes.OfferCurrency(Currency.getInstance("EUR")),
OfferTypes.OfferAmount(86205),
Comment thread
t-bast marked this conversation as resolved.
OfferTypes.OfferDescription("offer with a lot of fields in it"),
OfferTypes.OfferFeatures(Features(Features.ProvideStorage -> FeatureSupport.Mandatory)),
OfferTypes.OfferAbsoluteExpiry(TimestampSecond(3600)),
OfferTypes.OfferPaths(Seq(Sphinx.RouteBlinding.BlindedRoute(EncodedNodeId.WithPublicKey.Plain(PublicKey(hex"022812e3a3760ac989b8749ee9fc70fd12e4d7f3cad5e3e2bf572e9e4eaaa7b7d9")), PublicKey(hex"028a2b20b2debdfd97de08f6e2374f2946116492f358b78acf9eac05f6fdac632d"), Seq(Sphinx.RouteBlinding.BlindedHop(PublicKey(hex"031b27d9e97dbb0ef87c48bb0231c96c6bca1ee54b0e0cfe869ad2388ce247719f"), hex"def5"))))),
OfferTypes.OfferIssuer("bob@bobcorp.com"),
OfferTypes.OfferQuantityMax(5),
OfferTypes.OfferNodeId(PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")),
), Set(GenericTlv(UInt64(71), hex"bd4e85ce"))))
JsonSerializers.serialization.write(bigOffer)(JsonSerializers.formats) shouldBe """{"chains":["43f08bdab050e35b567c864b91f47f50ae725ae2de53bcfbbaf284da00000000"],"amount":"862.05","currency":"EUR","description":"offer with a lot of fields in it","expiry":{"iso":"1970-01-01T01:00:00Z","unix":3600},"issuer":"bob@bobcorp.com","nodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f","paths":[{"firstNodeId":{"publicKey":"022812e3a3760ac989b8749ee9fc70fd12e4d7f3cad5e3e2bf572e9e4eaaa7b7d9"},"length":1}],"quantityMax":5,"features":{"activated":{"option_provide_storage":"mandatory"},"unknown":[]},"metadata":"d5f4a6","unknownTlvs":{"71":"bd4e85ce"}}"""
}

test("Bolt 12 offer data") {
val ref = "lno1pqzqzltcgq9q6urvv4shxefqv3hkuct5v58qxrp4qqfquctvd93k2srpvd5kuufwvdh3vggzg2hd49ueds8phzcahvh4p2m3pnen649dza2h3k6gxpaequr8fhtq"
val offer = OfferData(Offer.decode(ref).get, None, createdAt = TimestampMilli(100), disabledAt_opt = None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
// changing fields makes the signature invalid
val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Set(GenericTlv(UInt64(7), hex"ade4"))))
assert(!withModifiedUnknownTlv.checkSignature())
val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(amount) => OfferAmount(amount + 100.msat) case x => x }, invoice.records.unknown))
val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(amount) => OfferAmount(amount + 100) case x => x }, invoice.records.unknown))
assert(!withModifiedAmount.checkSignature())
}

Expand All @@ -92,7 +92,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
assert(invoice.validateFor(request, nodeKey.publicKey).isRight)
// amount must match the request
val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(_) => OfferAmount(9000 msat) case x => x })), nodeKey)
val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(_) => OfferAmount(9000) case x => x })), nodeKey)
assert(withOtherAmount.validateFor(request, nodeKey.publicKey).isLeft)
// description must match the offer
val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferDescription(_) => OfferDescription("other description") case x => x })), nodeKey)
Expand Down Expand Up @@ -229,7 +229,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
val tlvs = TlvStream[InvoiceTlv](Set[InvoiceTlv](
InvoiceRequestMetadata(payerInfo),
OfferChains(Seq(chain)),
OfferAmount(amount),
OfferAmount(amount.toLong),
OfferDescription(description),
OfferFeatures(Features.empty),
OfferIssuer(issuer),
Expand Down Expand Up @@ -339,7 +339,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
val preimage = ByteVector32(hex"99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8")
val offer = Offer(TlvStream[OfferTlv](
OfferChains(Seq(Block.Testnet3GenesisBlock.hash)),
OfferAmount(100000 msat),
OfferAmount(100000),
OfferDescription("offer with quantity"),
OfferIssuer("alice@bigshop.com"),
OfferQuantityMax(1000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class OfferTypesSpec extends AnyFunSuite {
test("offer with amount and quantity") {
val offer = Offer(TlvStream[OfferTlv](
OfferChains(Seq(Block.Testnet3GenesisBlock.hash)),
OfferAmount(50 msat),
OfferAmount(50),
OfferDescription("offer with quantity"),
OfferIssuer("alice@bigshop.com"),
OfferQuantityMax(0),
Expand Down Expand Up @@ -131,7 +131,7 @@ class OfferTypesSpec extends AnyFunSuite {

test("check that invoice request matches offer (chain compatibility)") {
{
val offer = Offer(TlvStream(OfferAmount(100 msat), OfferDescription("offer without chains"), OfferNodeId(randomKey().publicKey)))
val offer = Offer(TlvStream(OfferAmount(100), OfferDescription("offer without chains"), OfferNodeId(randomKey().publicKey)))
val payerKey = randomKey()
val request = {
val tlvs: Set[InvoiceRequestTlv] = offer.records.records ++ Set(
Expand All @@ -152,7 +152,7 @@ class OfferTypesSpec extends AnyFunSuite {
}
{
val (chain1, chain2) = (BlockHash(randomBytes32()), BlockHash(randomBytes32()))
val offer = Offer(TlvStream(OfferChains(Seq(chain1, chain2)), OfferAmount(100 msat), OfferDescription("offer with chains"), OfferNodeId(randomKey().publicKey)))
val offer = Offer(TlvStream(OfferChains(Seq(chain1, chain2)), OfferAmount(100), OfferDescription("offer with chains"), OfferNodeId(randomKey().publicKey)))
val payerKey = randomKey()
val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1)
assert(request1.isValid)
Expand All @@ -169,7 +169,7 @@ class OfferTypesSpec extends AnyFunSuite {

test("check that invoice request matches offer (multiple items)") {
val offer = Offer(TlvStream(
OfferAmount(500 msat),
OfferAmount(500),
OfferDescription("offer for multiple items"),
OfferNodeId(randomKey().publicKey),
OfferQuantityMax(10),
Expand Down Expand Up @@ -314,13 +314,8 @@ class OfferTypesSpec extends AnyFunSuite {
val testVectors = JsonMethods.parse(src.mkString).extract[Seq[TestVector]]
src.close()
for (vector <- testVectors) {
if (vector.description == "with currency") {
// We don't support currency conversion yet.
assert(Offer.decode(vector.bolt12).isFailure)
} else {
val offer = Offer.decode(vector.bolt12)
assert((offer.isSuccess && offer.get.features.unknown.forall(_.bitIndex % 2 == 1)) == vector.valid, vector.description)
}
val offer = Offer.decode(vector.bolt12)
assert((offer.isSuccess && offer.get.features.unknown.forall(_.bitIndex % 2 == 1)) == vector.valid, vector.description)
}
}

Expand Down
Loading