diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index 7cff2d348..599870cd1 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -673,7 +673,7 @@ object JsonSerializers { data class OfferSurrogate( val chain: String, val chainHashes: List?, - val amount: MilliSatoshi?, + val amount: Long?, val currency: String?, val issuer: String?, val quantityMax: Long?, @@ -698,7 +698,7 @@ object JsonSerializers { else -> "unknown" }.lowercase(), chainHashes = o.records.get()?.chains, - amount = o.amount, + amount = o.records.get()?.amount, currency = o.currency, issuer = o.issuer, quantityMax = o.quantityMax, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt index 9affd2ae3..7fb426b6b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt @@ -108,8 +108,8 @@ data class Bolt12Invoice(val records: TlvStream) : PaymentRequest() additionalTlvs: Set = setOf(), customTlvs: Set = setOf() ): Bolt12Invoice { - require(request.amount != null || request.offer.amount != null) - val amount = request.amount ?: (request.offer.amount!! * request.quantity) + require(request.amount != null || request.offer.amountMsat != null) + val amount = request.amount ?: (request.offer.amountMsat!! * request.quantity) val tlvs: Set = removeSignature(request.records).records + setOfNotNull( InvoicePaths(paths.map { it.route }), InvoiceBlindedPay(paths.map { it.paymentInfo }), diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt index 8e38b2390..9760ebbde 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt @@ -232,7 +232,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v private fun isOurOffer(offer: OfferTypes.Offer, pathId: ByteVector?, blindedPrivateKey: PrivateKey): Boolean = when { pathId != null && pathId.size() != 32 -> false else -> { - val expected = deterministicOffer(nodeParams.chainHash, nodeParams.nodePrivateKey, walletParams.trampolineNode.id, offer.amount, offer.description, pathId?.let { ByteVector32(it) }) + val expected = deterministicOffer(nodeParams.chainHash, nodeParams.nodePrivateKey, walletParams.trampolineNode.id, offer.amountMsat, offer.description, pathId?.let { ByteVector32(it) }) expected == Pair(offer, blindedPrivateKey) } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt index 807ca43b1..7b11a5d73 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt @@ -133,17 +133,17 @@ object OfferTypes { /** * Amount to pay per item. As we only support bitcoin, the amount is in msat. */ - data class OfferAmount(val amount: MilliSatoshi) : OfferTlv() { + data class OfferAmount(val amount: Long) : OfferTlv() { override val tag: Long get() = OfferAmount.tag override fun write(out: Output) { - LightningCodecs.writeTU64(amount.toLong(), out) + LightningCodecs.writeTU64(amount, out) } companion object : TlvValueReader { const val tag: Long = 8 override fun read(input: Input): OfferAmount { - return OfferAmount(MilliSatoshi(LightningCodecs.tu64(input))) + return OfferAmount(LightningCodecs.tu64(input)) } } } @@ -725,11 +725,12 @@ object OfferTypes { val chains: List = records.get()?.chains ?: listOf(Block.LivenetGenesisBlock.hash) val metadata: ByteVector? = records.get()?.data val currency: String? = records.get()?.iso4217 - val amount: MilliSatoshi? = if (currency == null) { - records.get()?.amount + val amount: Either? = if (currency == null) { + records.get()?.amount?.let { Left(MilliSatoshi(it)) } } else { - null // TODO: add exchange rates + records.get()?.amount?.let { Right(it) } } + val amountMsat: MilliSatoshi? = amount?.left val description: String? = records.get()?.description val features: Features = records.get()?.features?.let { Features(it) } ?: Features.empty val expirySeconds: Long? = records.get()?.absoluteExpirySeconds @@ -774,7 +775,7 @@ object OfferTypes { if (description == null) require(amount == null) { "an offer description must be provided if the amount isn't null" } val tlvs: Set = setOfNotNull( if (chain != Block.LivenetGenesisBlock.hash) OfferChains(listOf(chain)) else null, - amount?.let { OfferAmount(it) }, + amount?.let { OfferAmount(it.toLong()) }, description?.let { OfferDescription(it) }, features.bolt12Features().let { if (it != Features.empty) OfferFeatures(it.toByteArray().toByteVector()) else null }, OfferIssuerId(nodeId) @@ -810,7 +811,7 @@ object OfferTypes { ) val tlvs = setOfNotNull( if (chainHash != Block.LivenetGenesisBlock.hash) OfferChains(listOf(chainHash)) else null, - amount?.let { OfferAmount(it) }, + amount?.let { OfferAmount(it.toLong()) }, description?.let { OfferDescription(it) }, features.bolt12Features().let { if (it != Features.empty) OfferFeatures(it.toByteArray().toByteVector()) else null }, // Note that we don't include an offer_node_id since we're using a blinded path. @@ -878,14 +879,14 @@ object OfferTypes { val quantity_opt: Long? = records.get()?.quantity val quantity: Long = quantity_opt ?: 1 // A valid invoice_request must either specify an amount, or the offer itself must specify an amount. - val requestedAmount: MilliSatoshi = amount ?: (offer.amount!! * quantity) + val requestedAmount: MilliSatoshi = amount ?: (offer.amountMsat!! * quantity) val payerId: PublicKey = records.get()!!.publicKey val payerNote: String? = records.get()?.note private val signature: ByteVector64 = records.get()!!.signature fun isValid(): Boolean = - (offer.amount == null || amount == null || offer.amount * quantity <= amount) && - (offer.amount != null || amount != null) && + (offer.amountMsat == null || amount == null || offer.amountMsat * quantity <= amount) && + (offer.amountMsat != null || amount != null) && offer.chains.contains(chain) && ((offer.quantityMax == null && quantity_opt == null) || (offer.quantityMax != null && quantity_opt != null && quantity <= offer.quantityMax)) && Features.areCompatible(offer.features, features) && diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt index 6ce95cf75..8c57445c4 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt @@ -109,7 +109,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { assertFalse(withModifiedUnknownTlv.checkSignature()) val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { when (it) { - is OfferAmount -> OfferAmount(it.amount + 100.msat) + is OfferAmount -> OfferAmount(it.amount + 100) else -> it } }.toSet(), invoice.records.unknown)) @@ -156,7 +156,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { // amount must match the request val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { when (it) { - is OfferAmount -> OfferAmount(9000.msat) + is OfferAmount -> OfferAmount(9000) else -> it } }.toSet())), nodeKey) @@ -356,7 +356,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { setOf( InvoiceRequestMetadata(payerInfo), OfferChains(listOf(chain)), - OfferAmount(amount), + OfferAmount(amount.toLong()), OfferDescription(description), OfferFeatures(ByteVector.empty), OfferIssuer(issuer), @@ -485,7 +485,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val offer = Offer( TlvStream( OfferChains(listOf(Block.Testnet3GenesisBlock.hash)), - OfferAmount(100000.msat), + OfferAmount(100000), OfferDescription("offer with quantity"), OfferIssuer("alice@bigshop.com"), OfferQuantityMax(1000), diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt index b596c89b3..05c6d79e6 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt @@ -5,6 +5,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey @@ -27,6 +28,7 @@ import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestTlv import fr.acinq.lightning.wire.OfferTypes.Offer import fr.acinq.lightning.wire.OfferTypes.OfferAmount import fr.acinq.lightning.wire.OfferTypes.OfferChains +import fr.acinq.lightning.wire.OfferTypes.OfferCurrency import fr.acinq.lightning.wire.OfferTypes.OfferDescription import fr.acinq.lightning.wire.OfferTypes.OfferIssuer import fr.acinq.lightning.wire.OfferTypes.OfferIssuerId @@ -71,7 +73,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { val offer = Offer( TlvStream( OfferChains(listOf(Block.Testnet3GenesisBlock.hash)), - OfferAmount(50.msat), + OfferAmount(50), OfferDescription("offer with quantity"), OfferIssuer("alice@bigshop.com"), OfferQuantityMax(0), @@ -80,7 +82,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { ) val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968jys3v9kxjcm9gp3xjemndphhqtnrdak3gqqkyypsmuhrtwfzm85mht4a3vcp0yrlgua3u3m5uqpc6kf7nqjz6v70qwg" assertEquals(offer, Offer.decode(encoded).get()) - assertEquals(50.msat, offer.amount) + assertEquals(Either.Left(50.msat), offer.amount) assertEquals("offer with quantity", offer.description) assertEquals(nodeId, offer.issuerId) assertEquals("alice@bigshop.com", offer.issuer) @@ -147,7 +149,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { @Test fun `check that invoice request matches offer - without chain`() { - val offer = Offer(TlvStream(OfferAmount(100.msat), OfferDescription("offer without chains"), OfferIssuerId(randomKey().publicKey()))) + val offer = Offer(TlvStream(OfferAmount(100), OfferDescription("offer without chains"), OfferIssuerId(randomKey().publicKey()))) val payerKey = randomKey() val tlvs: Set = offer.records.records + setOf( InvoiceRequestMetadata(ByteVector.fromHex("012345")), @@ -169,7 +171,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { fun `check that invoice request matches offer - with chains`() { val chain1 = BlockHash(randomBytes32()) val chain2 = BlockHash(randomBytes32()) - val offer = Offer(TlvStream(OfferChains(listOf(chain1, chain2)), OfferAmount(100.msat), OfferDescription("offer with chains"), OfferIssuerId(randomKey().publicKey()))) + val offer = Offer(TlvStream(OfferChains(listOf(chain1, chain2)), OfferAmount(100), OfferDescription("offer with chains"), OfferIssuerId(randomKey().publicKey()))) val payerKey = randomKey() val request1 = InvoiceRequest(offer, 100.msat, 1, Features.empty, payerKey, null, chain1) assertTrue(request1.isValid()) @@ -192,7 +194,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { fun `check that invoice request matches offer - multiple items`() { val offer = Offer( TlvStream( - OfferAmount(500.msat), + OfferAmount(500), OfferDescription("offer for multiple items"), OfferIssuerId(randomKey().publicKey()), OfferQuantityMax(10), @@ -614,4 +616,15 @@ class OfferTypesTestsCommon : LightningTestSuite() { assertTrue(Offer.decode(it).isFailure) } } + + @Test + fun `offer with fiat currency`(){ + val offer = Offer(TlvStream( + OfferAmount(123), + OfferCurrency("EUR"), + OfferDescription("offer for 1.23€"), + OfferIssuerId(randomKey().publicKey()))) + val invoiceRequest = InvoiceRequest(offer, 1269486.msat, 1, Features.empty, randomKey(), null, Block.LivenetGenesisBlock.hash) + assertTrue(invoiceRequest.isValid()) + } } \ No newline at end of file