diff --git a/src/main/kotlin/xyz/block/moneyaddress/Protocol.kt b/src/main/kotlin/xyz/block/moneyaddress/Protocol.kt index fabd4e6..077844d 100644 --- a/src/main/kotlin/xyz/block/moneyaddress/Protocol.kt +++ b/src/main/kotlin/xyz/block/moneyaddress/Protocol.kt @@ -20,6 +20,7 @@ open class Protocol(open val scheme: String) { when (this) { ETHEREUM.scheme -> ETHEREUM LIGHTNING_ADDRESS.scheme -> LIGHTNING_ADDRESS + LIGHTNING_OFFER.scheme -> LIGHTNING_OFFER MOBILE_MONEY.scheme -> MOBILE_MONEY ONCHAIN_ADDRESS.scheme -> ONCHAIN_ADDRESS SILENT_PAYMENT_ADDRESS.scheme -> SILENT_PAYMENT_ADDRESS @@ -31,6 +32,7 @@ open class Protocol(open val scheme: String) { data object ETHEREUM : Protocol("eth") data object LIGHTNING_ADDRESS : Protocol("lnaddr") +data object LIGHTNING_OFFER : Protocol("lno") data object MOBILE_MONEY : Protocol("momo") data object ONCHAIN_ADDRESS : Protocol("addr") data object SILENT_PAYMENT_ADDRESS : Protocol("spaddr") diff --git a/src/main/kotlin/xyz/block/moneyaddress/TypedMoneyAddressRegistry.kt b/src/main/kotlin/xyz/block/moneyaddress/TypedMoneyAddressRegistry.kt index 523b3e4..87aa8e2 100644 --- a/src/main/kotlin/xyz/block/moneyaddress/TypedMoneyAddressRegistry.kt +++ b/src/main/kotlin/xyz/block/moneyaddress/TypedMoneyAddressRegistry.kt @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import xyz.block.moneyaddress.Currency.Companion.asCurrency import xyz.block.moneyaddress.Protocol.Companion.asProtocol import xyz.block.moneyaddress.typed.BtcLightningAddress +import xyz.block.moneyaddress.typed.BtcLightningOffer import xyz.block.moneyaddress.typed.BtcOnChainAddress import xyz.block.moneyaddress.typed.MobileMoneyAddress import xyz.block.moneyaddress.MoneyAddress as UntypedMoneyAddress @@ -68,6 +69,7 @@ class TypedMoneyAddressRegistry { init { BtcLightningAddress.register(defaultTypedMoneyAddressRegistry) + BtcLightningOffer.register(defaultTypedMoneyAddressRegistry) BtcOnChainAddress.register(defaultTypedMoneyAddressRegistry) MobileMoneyAddress.register(defaultTypedMoneyAddressRegistry) } diff --git a/src/main/kotlin/xyz/block/moneyaddress/typed/BtcLightningOffer.kt b/src/main/kotlin/xyz/block/moneyaddress/typed/BtcLightningOffer.kt new file mode 100644 index 0000000..0e79e9b --- /dev/null +++ b/src/main/kotlin/xyz/block/moneyaddress/typed/BtcLightningOffer.kt @@ -0,0 +1,44 @@ +package xyz.block.moneyaddress.typed + +import xyz.block.moneyaddress.BTC +import xyz.block.moneyaddress.LIGHTNING_OFFER +import xyz.block.moneyaddress.TypedMoneyAddressRegistry +import xyz.block.moneyaddress.TypedMoneyAddress +import xyz.block.moneyaddress.matches +import xyz.block.moneyaddress.MoneyAddress as UntypedMoneyAddress + +/** + * A typed representation of a Bitcoin Lightning Offer + * i.e. a re-usable payment code from [BOLT 12](https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md) + * + * @property address - the string representation of the lightning offer. Not validated. + * @property currency - always [BTC]. + * @property protocol - always [LIGHTNING_OFFER]. + */ +data class BtcLightningOffer( + override val address: String, + override val id: String, +) : TypedMoneyAddress(address, BTC, LIGHTNING_OFFER, id) { + companion object { + fun register(typedMoneyAddressRegistry: TypedMoneyAddressRegistry) { + typedMoneyAddressRegistry.register(BTC, LIGHTNING_OFFER) { address -> from(address) } + } + + /** + * Converts an [UntypedMoneyAddress] to a [BtcLightningAddress]. + * + * @throws NotABtcLightningAddressException if the currency is not [BTC] or the protocol is not [LIGHTNING_OFFER]. + */ + fun from(untypedMoneyAddress: UntypedMoneyAddress): BtcLightningOffer { + if (!untypedMoneyAddress.matches(BTC, LIGHTNING_OFFER)) { + throw NotABtcLightningOfferException(untypedMoneyAddress) + } + return BtcLightningOffer(untypedMoneyAddress.pss, untypedMoneyAddress.id) + } + + fun UntypedMoneyAddress.toBtcLightningOffer(): BtcLightningOffer = from(this) + } +} + +class NotABtcLightningOfferException(moneyAddress: UntypedMoneyAddress) : + Throwable("Not a bitcoin lightning offer [$moneyAddress]") diff --git a/src/test/kotlin/xyz/block/moneyaddress/FilterTest.kt b/src/test/kotlin/xyz/block/moneyaddress/FilterTest.kt index 04f9831..85d987a 100644 --- a/src/test/kotlin/xyz/block/moneyaddress/FilterTest.kt +++ b/src/test/kotlin/xyz/block/moneyaddress/FilterTest.kt @@ -1,7 +1,9 @@ package xyz.block.moneyaddress -import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningMoneyAddress1 -import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningMoneyAddress2 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningAddressMoneyAddress1 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningAddressMoneyAddress2 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningOfferMoneyAddress1 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningOfferMoneyAddress2 import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcOnChainMoneyAddress1 import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcOnChainMoneyAddress2 import xyz.block.moneyaddress.MoneyAddressExamples.Companion.manyMoneyAddresses @@ -19,10 +21,12 @@ class FilterTest { val bitcoinAddresses = manyMoneyAddresses.filter { it.hasCurrency(BTC) } assertEquals( listOf( + btcLightningAddressMoneyAddress1, + btcLightningAddressMoneyAddress2, + btcLightningOfferMoneyAddress1, + btcLightningOfferMoneyAddress2, btcOnChainMoneyAddress1, btcOnChainMoneyAddress2, - btcLightningMoneyAddress1, - btcLightningMoneyAddress2, ), bitcoinAddresses ) diff --git a/src/test/kotlin/xyz/block/moneyaddress/MoneyAddressExamples.kt b/src/test/kotlin/xyz/block/moneyaddress/MoneyAddressExamples.kt index a7eaa13..e3229d4 100644 --- a/src/test/kotlin/xyz/block/moneyaddress/MoneyAddressExamples.kt +++ b/src/test/kotlin/xyz/block/moneyaddress/MoneyAddressExamples.kt @@ -6,28 +6,32 @@ import java.util.UUID class MoneyAddressExamples { companion object { + val btcLightningAddressMoneyAddress1 = makeMoneyAddress("btc", "lnaddr") + val btcLightningAddressMoneyAddress2 = makeMoneyAddress("btc", "lnaddr") + val btcLightningOfferMoneyAddress1 = makeMoneyAddress("btc", "lno") + val btcLightningOfferMoneyAddress2 = makeMoneyAddress("btc", "lno") val btcOnChainMoneyAddress1 = makeMoneyAddress("btc", "addr") val btcOnChainMoneyAddress2 = makeMoneyAddress("btc", "addr") - val btcLightningMoneyAddress1 = makeMoneyAddress("btc", "lnaddr") - val btcLightningMoneyAddress2 = makeMoneyAddress("btc", "lnaddr") - val usdcEthMoneyAddress1 = makeMoneyAddress("usdc", "eth") - val usdcEthMoneyAddress2 = makeMoneyAddress("usdc", "eth") val kesMomoMoneyAddress1 = makeMoneyAddress("kes", "momo", "mpesa:11111") val kesMomoMoneyAddress2 = makeMoneyAddress("kes", "momo", "mpesa:22222") + val usdcEthMoneyAddress1 = makeMoneyAddress("usdc", "eth") + val usdcEthMoneyAddress2 = makeMoneyAddress("usdc", "eth") val zarMomoMoneyAddress1 = makeMoneyAddress("zar", "momo", "mpesa:33333") val zarMomoMoneyAddress2 = makeMoneyAddress("zar", "momo", "mpesa:44444") val zzzUnrecognizedMoneyAddress1 = makeMoneyAddress("zzz", "zzz", "zzz1") val zzzUnrecognizedMoneyAddress2 = makeMoneyAddress("zzz", "zzz", "zzz2") val manyMoneyAddresses = listOf( + btcLightningAddressMoneyAddress1, + btcLightningAddressMoneyAddress2, + btcLightningOfferMoneyAddress1, + btcLightningOfferMoneyAddress2, btcOnChainMoneyAddress1, btcOnChainMoneyAddress2, - btcLightningMoneyAddress1, - btcLightningMoneyAddress2, - usdcEthMoneyAddress1, - usdcEthMoneyAddress2, kesMomoMoneyAddress1, kesMomoMoneyAddress2, + usdcEthMoneyAddress1, + usdcEthMoneyAddress2, zarMomoMoneyAddress1, zarMomoMoneyAddress2, zzzUnrecognizedMoneyAddress1, diff --git a/src/test/kotlin/xyz/block/moneyaddress/TypedTypedMoneyAddressRegistryTest.kt b/src/test/kotlin/xyz/block/moneyaddress/TypedTypedMoneyAddressRegistryTest.kt index 332025c..deefc26 100644 --- a/src/test/kotlin/xyz/block/moneyaddress/TypedTypedMoneyAddressRegistryTest.kt +++ b/src/test/kotlin/xyz/block/moneyaddress/TypedTypedMoneyAddressRegistryTest.kt @@ -1,8 +1,10 @@ package xyz.block.moneyaddress import xyz.block.moneyaddress.Currency.Companion.asCurrency -import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningMoneyAddress1 -import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningMoneyAddress2 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningAddressMoneyAddress1 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningAddressMoneyAddress2 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningOfferMoneyAddress1 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningOfferMoneyAddress2 import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcOnChainMoneyAddress1 import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcOnChainMoneyAddress2 import xyz.block.moneyaddress.MoneyAddressExamples.Companion.kesMomoMoneyAddress1 @@ -13,6 +15,7 @@ import xyz.block.moneyaddress.TypedMoneyAddressRegistry.Companion.defaultTypedMo import xyz.block.moneyaddress.TypedMoneyAddressRegistry.Companion.toTypedMoneyAddress import xyz.block.moneyaddress.Protocol.Companion.asProtocol import xyz.block.moneyaddress.typed.BtcLightningAddress +import xyz.block.moneyaddress.typed.BtcLightningOffer import xyz.block.moneyaddress.typed.BtcOnChainAddress import xyz.block.moneyaddress.typed.MPESA import xyz.block.moneyaddress.typed.MobileMoneyAddress @@ -25,10 +28,10 @@ class TypedTypedMoneyAddressRegistryTest { @Test fun toTypedAddress() { - val btcLightningAddress = btcLightningMoneyAddress1.toTypedMoneyAddress() + val btcLightningAddress = btcLightningAddressMoneyAddress1.toTypedMoneyAddress() assertIs(btcLightningAddress) assertEquals( - BtcLightningAddress(btcLightningMoneyAddress1.pss, btcLightningMoneyAddress1.id), + BtcLightningAddress(btcLightningAddressMoneyAddress1.pss, btcLightningAddressMoneyAddress1.id), btcLightningAddress ) @@ -43,7 +46,7 @@ class TypedTypedMoneyAddressRegistryTest { @Test fun toTypedAddressWithDefaultRegistry() { assertIs( - btcLightningMoneyAddress1.toTypedMoneyAddress(defaultTypedMoneyAddressRegistry) + btcLightningAddressMoneyAddress1.toTypedMoneyAddress(defaultTypedMoneyAddressRegistry) ) assertIs( kesMomoMoneyAddress1.toTypedMoneyAddress(defaultTypedMoneyAddressRegistry) @@ -71,10 +74,12 @@ class TypedTypedMoneyAddressRegistryTest { manyMoneyAddresses.map { it.toTypedMoneyAddress() }.filter { it.currency == BTC } assertEquals( listOf( + BtcLightningAddress(btcLightningAddressMoneyAddress1.pss, btcLightningAddressMoneyAddress1.id), + BtcLightningAddress(btcLightningAddressMoneyAddress2.pss, btcLightningAddressMoneyAddress2.id), + BtcLightningOffer(btcLightningOfferMoneyAddress1.pss, btcLightningOfferMoneyAddress1.id), + BtcLightningOffer(btcLightningOfferMoneyAddress2.pss, btcLightningOfferMoneyAddress2.id), BtcOnChainAddress(btcOnChainMoneyAddress1.pss, btcOnChainMoneyAddress1.id), BtcOnChainAddress(btcOnChainMoneyAddress2.pss, btcOnChainMoneyAddress2.id), - BtcLightningAddress(btcLightningMoneyAddress1.pss, btcLightningMoneyAddress1.id), - BtcLightningAddress(btcLightningMoneyAddress2.pss, btcLightningMoneyAddress2.id), ), bitcoinAddresses ) @@ -97,8 +102,8 @@ class TypedTypedMoneyAddressRegistryTest { fun toTypedAddressWithFilteringByIsInstance() { assertEquals( listOf( - BtcLightningAddress(btcLightningMoneyAddress1.pss, btcLightningMoneyAddress1.id), - BtcLightningAddress(btcLightningMoneyAddress2.pss, btcLightningMoneyAddress2.id), + BtcLightningAddress(btcLightningAddressMoneyAddress1.pss, btcLightningAddressMoneyAddress1.id), + BtcLightningAddress(btcLightningAddressMoneyAddress2.pss, btcLightningAddressMoneyAddress2.id), ), manyMoneyAddresses.map { it.toTypedMoneyAddress() }.filterIsInstance() ) diff --git a/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningAddressTest.kt b/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningAddressTest.kt index 1afabd7..b84eccf 100644 --- a/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningAddressTest.kt +++ b/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningAddressTest.kt @@ -1,6 +1,6 @@ package xyz.block.moneyaddress.typed -import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningMoneyAddress1 +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningAddressMoneyAddress1 import xyz.block.moneyaddress.typed.BtcLightningAddress.Companion.toBtcLightningAddress import kotlin.test.Test import kotlin.test.assertEquals @@ -9,15 +9,15 @@ class BtcLightningAddressTest { @Test fun toBtcLightningAddress() { assertEquals( - BtcLightningAddress(btcLightningMoneyAddress1.pss, btcLightningMoneyAddress1.id), - btcLightningMoneyAddress1.toBtcLightningAddress() + BtcLightningAddress(btcLightningAddressMoneyAddress1.pss, btcLightningAddressMoneyAddress1.id), + btcLightningAddressMoneyAddress1.toBtcLightningAddress() ) } @Test fun fromUntypedMoneyAddress() { assertEquals( - BtcLightningAddress(btcLightningMoneyAddress1.pss, btcLightningMoneyAddress1.id), - BtcLightningAddress.from(btcLightningMoneyAddress1) + BtcLightningAddress(btcLightningAddressMoneyAddress1.pss, btcLightningAddressMoneyAddress1.id), + BtcLightningAddress.from(btcLightningAddressMoneyAddress1) ) } } diff --git a/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningOfferTest.kt b/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningOfferTest.kt new file mode 100644 index 0000000..5eaa313 --- /dev/null +++ b/src/test/kotlin/xyz/block/moneyaddress/typed/BtcLightningOfferTest.kt @@ -0,0 +1,23 @@ +package xyz.block.moneyaddress.typed + +import xyz.block.moneyaddress.MoneyAddressExamples.Companion.btcLightningOfferMoneyAddress1 +import xyz.block.moneyaddress.typed.BtcLightningOffer.Companion.toBtcLightningOffer +import kotlin.test.Test +import kotlin.test.assertEquals + +class BtcLightningOfferTest { + @Test + fun toBtcLightningOffer() { + assertEquals( + BtcLightningOffer(btcLightningOfferMoneyAddress1.pss, btcLightningOfferMoneyAddress1.id), + btcLightningOfferMoneyAddress1.toBtcLightningOffer() + ) + } + @Test + fun fromUntypedMoneyOffer() { + assertEquals( + BtcLightningOffer(btcLightningOfferMoneyAddress1.pss, btcLightningOfferMoneyAddress1.id), + BtcLightningOffer.from(btcLightningOfferMoneyAddress1) + ) + } +}