test: add comprehensive test suite for NIP-47 Wallet Connect implementation

Covers all 10 request methods (create, serialize, deserialize), all success
and error response types, notification deserialization, EncryptionTag and
NotificationsTag parse/assemble/isTag, NwcInfoEvent build and capabilities,
NwcNotificationEvent structure, LnZapPaymentRequestEvent create/decrypt
with NIP-04 and NIP-44, and Nip47WalletConnect URI parsing and serialization.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
This commit is contained in:
Claude
2026-03-13 03:45:50 +00:00
parent da26ab14ab
commit 8c73dd8f14
9 changed files with 1502 additions and 0 deletions
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class LnZapPaymentRequestEventTest {
@Test
fun testEventKind() {
assertEquals(23194, LnZapPaymentRequestEvent.KIND)
}
@Test
fun testCreatePayInvoiceRequest() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val event =
LnZapPaymentRequestEvent.create(
lnInvoice = "lnbc50n1...",
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
createdAt = 1000L,
)
assertEquals(23194, event.kind)
assertEquals(walletServicePubkey, event.walletServicePubKey())
assertTrue(event.isContentEncoded())
assertNotNull(event.content)
assertTrue(event.content.isNotEmpty())
}
@Test
fun testCreateGenericRequest() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val request = GetBalanceMethod.create()
val event =
LnZapPaymentRequestEvent.createRequest(
request = request,
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
createdAt = 1000L,
)
assertEquals(23194, event.kind)
assertEquals(walletServicePubkey, event.walletServicePubKey())
// Should not have encryption tag for NIP-04
assertNull(event.encryptionScheme())
}
@Test
fun testCreateRequestWithNip44() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val request = GetBalanceMethod.create()
val event =
LnZapPaymentRequestEvent.createRequest(
request = request,
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
createdAt = 1000L,
useNip44 = true,
)
assertEquals(23194, event.kind)
assertEquals("nip44_v2", event.encryptionScheme())
}
@Test
fun testDecryptPayInvoiceRequest() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val event =
LnZapPaymentRequestEvent.create(
lnInvoice = "lnbc50n1...",
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
)
// Wallet service should be able to decrypt
val decrypted = event.decryptRequest(walletSigner)
assertIs<PayInvoiceMethod>(decrypted)
assertEquals("lnbc50n1...", decrypted.params?.invoice)
}
@Test
fun testDecryptGenericRequest() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val request = MakeInvoiceMethod.create(5000L, "test payment")
val event =
LnZapPaymentRequestEvent.createRequest(
request = request,
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
)
val decrypted = event.decryptRequest(walletSigner)
assertIs<MakeInvoiceMethod>(decrypted)
assertEquals(5000L, decrypted.params?.amount)
assertEquals("test payment", decrypted.params?.description)
}
@Test
fun testDecryptNip44Request() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val request = GetInfoMethod.create()
val event =
LnZapPaymentRequestEvent.createRequest(
request = request,
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
useNip44 = true,
)
assertEquals("nip44_v2", event.encryptionScheme())
// Wallet service should be able to decrypt NIP-44 encrypted request
val decrypted = event.decryptRequest(walletSigner)
assertIs<GetInfoMethod>(decrypted)
}
@Test
fun testCanDecrypt() =
runTest {
val clientKeyPair = KeyPair()
val walletKeyPair = KeyPair()
val otherKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val otherSigner = NostrSignerInternal(otherKeyPair)
val walletServicePubkey: HexKey =
walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
val event =
LnZapPaymentRequestEvent.create(
lnInvoice = "lnbc50n1...",
walletServicePubkey = walletServicePubkey,
signer = clientSigner,
)
assertTrue(event.canDecrypt(clientSigner))
assertTrue(event.canDecrypt(walletSigner))
assertTrue(!event.canDecrypt(otherSigner))
}
}
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class Nip47WalletConnectTest {
@Test
fun testParseWalletConnectUri() {
val uri =
"nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100571c5"
val parsed = Nip47WalletConnect.parse(uri)
assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex)
assertEquals("wss://relay.damus.io/", parsed.relayUri.url)
assertEquals("71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100571c5", parsed.secret)
assertNull(parsed.lud16)
}
@Test
fun testParseWalletConnectUriWithLud16() {
val uri =
"nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100571c5&lud16=user%40example.com"
val parsed = Nip47WalletConnect.parse(uri)
assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex)
assertNotNull(parsed.lud16)
assertEquals("user@example.com", parsed.lud16)
}
@Test
fun testParseNostrWalletConnectScheme() {
val uri =
"nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=abc"
val parsed = Nip47WalletConnect.parse(uri)
assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex)
}
@Test
fun testParseAmethystWalletConnectScheme() {
val uri =
"amethyst+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=abc"
val parsed = Nip47WalletConnect.parse(uri)
assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex)
}
@Test
fun testParseWithoutSecret() {
val uri =
"nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io"
val parsed = Nip47WalletConnect.parse(uri)
assertNull(parsed.secret)
}
@Test
fun testParseInvalidSchemeThrows() {
val uri = "https://example.com?relay=wss%3A%2F%2Frelay.damus.io"
assertFailsWith<IllegalArgumentException> {
Nip47WalletConnect.parse(uri)
}
}
@Test
fun testParseWithoutRelayThrows() {
val uri = "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4"
assertFailsWith<IllegalArgumentException> {
Nip47WalletConnect.parse(uri)
}
}
// --- Nip47URI serialization ---
@Test
fun testNip47UriSerializationRoundTrip() {
val original =
Nip47WalletConnect.Nip47URI(
pubKeyHex = "b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4",
relayUri = "wss://relay.damus.io",
secret = "abc123",
lud16 = "user@example.com",
)
val json = Nip47WalletConnect.Nip47URI.serializer(original)
val deserialized = Nip47WalletConnect.Nip47URI.parser(json)
assertEquals(original.pubKeyHex, deserialized.pubKeyHex)
assertEquals(original.relayUri, deserialized.relayUri)
assertEquals(original.secret, deserialized.secret)
assertEquals(original.lud16, deserialized.lud16)
}
@Test
fun testNip47UriSerializationWithNullLud16() {
val original =
Nip47WalletConnect.Nip47URI(
pubKeyHex = "abc123",
relayUri = "wss://relay.damus.io",
secret = "secret",
)
val json = Nip47WalletConnect.Nip47URI.serializer(original)
val deserialized = Nip47WalletConnect.Nip47URI.parser(json)
assertEquals(original.pubKeyHex, deserialized.pubKeyHex)
assertNull(deserialized.lud16)
}
// --- Normalize/Denormalize ---
@Test
fun testNormalizeDenormalizeRoundTrip() {
val uri =
Nip47WalletConnect.Nip47URI(
pubKeyHex = "abc123",
relayUri = "wss://relay.damus.io",
secret = "secret",
lud16 = "user@example.com",
)
val normalized = uri.normalize()
assertNotNull(normalized)
assertEquals("user@example.com", normalized.lud16)
val denormalized = normalized.denormalize()
assertNotNull(denormalized)
assertEquals("abc123", denormalized.pubKeyHex)
assertEquals("secret", denormalized.secret)
assertEquals("user@example.com", denormalized.lud16)
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class NotificationTest {
@Test
fun testPaymentReceivedDeserialization() {
val json =
"""{"notification_type":"payment_received","notification":{"type":"incoming","invoice":"lnbc50n1...","description":"coffee","preimage":"abc","payment_hash":"hash123","amount":5000,"fees_paid":10,"created_at":1693876497,"expires_at":1694876497,"settled_at":1694876500}}"""
val notification = OptimizedJsonMapper.fromJsonTo<Notification>(json)
assertIs<PaymentReceivedNotification>(notification)
assertNotNull(notification.notification)
assertEquals("incoming", notification.notification?.type)
assertEquals("lnbc50n1...", notification.notification?.invoice)
assertEquals("coffee", notification.notification?.description)
assertEquals("abc", notification.notification?.preimage)
assertEquals("hash123", notification.notification?.payment_hash)
assertEquals(5000L, notification.notification?.amount)
assertEquals(10L, notification.notification?.fees_paid)
assertEquals(1693876497L, notification.notification?.created_at)
assertEquals(1694876497L, notification.notification?.expires_at)
assertEquals(1694876500L, notification.notification?.settled_at)
}
@Test
fun testPaymentSentDeserialization() {
val json =
"""{"notification_type":"payment_sent","notification":{"type":"outgoing","invoice":"lnbc100n1...","preimage":"def456","payment_hash":"hash456","amount":10000,"fees_paid":50,"created_at":1693876497,"settled_at":1694876500}}"""
val notification = OptimizedJsonMapper.fromJsonTo<Notification>(json)
assertIs<PaymentSentNotification>(notification)
assertNotNull(notification.notification)
assertEquals("outgoing", notification.notification?.type)
assertEquals("lnbc100n1...", notification.notification?.invoice)
assertEquals("def456", notification.notification?.preimage)
assertEquals(10000L, notification.notification?.amount)
assertEquals(50L, notification.notification?.fees_paid)
}
@Test
fun testHoldInvoiceAcceptedDeserialization() {
val json =
"""{"notification_type":"hold_invoice_accepted","notification":{"type":"incoming","invoice":"lnbc200n1...","payment_hash":"hash789","amount":20000,"created_at":1693876497,"expires_at":1694876497,"settle_deadline":800000}}"""
val notification = OptimizedJsonMapper.fromJsonTo<Notification>(json)
assertIs<HoldInvoiceAcceptedNotification>(notification)
assertNotNull(notification.notification)
assertEquals("incoming", notification.notification?.type)
assertEquals("lnbc200n1...", notification.notification?.invoice)
assertEquals("hash789", notification.notification?.payment_hash)
assertEquals(20000L, notification.notification?.amount)
assertEquals(800000L, notification.notification?.settle_deadline)
assertEquals(1693876497L, notification.notification?.created_at)
assertEquals(1694876497L, notification.notification?.expires_at)
}
@Test
fun testPaymentReceivedMinimalFields() {
val json = """{"notification_type":"payment_received","notification":{"type":"incoming","amount":100}}"""
val notification = OptimizedJsonMapper.fromJsonTo<Notification>(json)
assertIs<PaymentReceivedNotification>(notification)
assertEquals("incoming", notification.notification?.type)
assertEquals(100L, notification.notification?.amount)
assertNull(notification.notification?.invoice)
assertNull(notification.notification?.preimage)
}
@Test
fun testUnknownNotificationTypeReturnsNull() {
val json = """{"notification_type":"unknown_type","notification":{}}"""
val notification = OptimizedJsonMapper.fromJsonTo<Notification>(json)
assertNull(notification)
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.utils.DeterministicSigner
import com.vitorpamplona.quartz.utils.nsecToKeyPair
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NwcInfoEventTest {
private val signer = DeterministicSigner("nsec1w4uucmeyyng0kegm7486r23sv4majkmvqsj6eypprq0xttxss55s5mgg9t".nsecToKeyPair())
@Test
fun testBuildInfoEvent() {
val capabilities = listOf("pay_invoice", "get_balance", "make_invoice", "notifications")
val template = NwcInfoEvent.build(capabilities)
val event = signer.sign<NwcInfoEvent>(template)
assertEquals(NwcInfoEvent.KIND, event.kind)
assertEquals("pay_invoice get_balance make_invoice notifications", event.content)
}
@Test
fun testCapabilities() {
val capabilities = listOf("pay_invoice", "get_balance", "make_invoice")
val template = NwcInfoEvent.build(capabilities)
val event = signer.sign<NwcInfoEvent>(template)
val parsed = event.capabilities()
assertEquals(3, parsed.size)
assertTrue(parsed.contains("pay_invoice"))
assertTrue(parsed.contains("get_balance"))
assertTrue(parsed.contains("make_invoice"))
}
@Test
fun testSupportsMethod() {
val capabilities = listOf("pay_invoice", "get_balance")
val template = NwcInfoEvent.build(capabilities)
val event = signer.sign<NwcInfoEvent>(template)
assertTrue(event.supportsMethod("pay_invoice"))
assertTrue(event.supportsMethod("get_balance"))
assertFalse(event.supportsMethod("make_invoice"))
assertFalse(event.supportsMethod("pay_keysend"))
}
@Test
fun testSupportsNotifications() {
val capabilities = listOf("pay_invoice", "notifications")
val template = NwcInfoEvent.build(capabilities)
val event = signer.sign<NwcInfoEvent>(template)
assertTrue(event.supportsNotifications())
}
@Test
fun testDoesNotSupportNotifications() {
val capabilities = listOf("pay_invoice", "get_balance")
val template = NwcInfoEvent.build(capabilities)
val event = signer.sign<NwcInfoEvent>(template)
assertFalse(event.supportsNotifications())
}
@Test
fun testEncryptionSchemes() {
val capabilities = listOf("pay_invoice")
val template = NwcInfoEvent.build(capabilities, encryptionSchemes = listOf("nip44_v2", "nip04"))
val event = signer.sign<NwcInfoEvent>(template)
val schemes = event.encryptionSchemes()
assertEquals(2, schemes.size)
assertTrue(schemes.contains("nip44_v2"))
assertTrue(schemes.contains("nip04"))
}
@Test
fun testNotificationTypes() {
val capabilities = listOf("pay_invoice", "notifications")
val template = NwcInfoEvent.build(capabilities, notificationTypes = listOf("payment_received", "payment_sent"))
val event = signer.sign<NwcInfoEvent>(template)
val types = event.notificationTypes()
assertEquals(2, types.size)
assertTrue(types.contains("payment_received"))
assertTrue(types.contains("payment_sent"))
}
@Test
fun testInfoEventKind() {
assertEquals(13194, NwcInfoEvent.KIND)
}
@Test
fun testBuildWithNoOptionalTags() {
val capabilities = listOf("pay_invoice")
val template = NwcInfoEvent.build(capabilities)
val event = signer.sign<NwcInfoEvent>(template)
assertTrue(event.encryptionSchemes().isEmpty())
assertTrue(event.notificationTypes().isEmpty())
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import kotlin.test.Test
import kotlin.test.assertEquals
class NwcMethodTest {
@Test
fun testMethodConstants() {
assertEquals("pay_invoice", NwcMethod.PAY_INVOICE)
assertEquals("pay_keysend", NwcMethod.PAY_KEYSEND)
assertEquals("make_invoice", NwcMethod.MAKE_INVOICE)
assertEquals("lookup_invoice", NwcMethod.LOOKUP_INVOICE)
assertEquals("list_transactions", NwcMethod.LIST_TRANSACTIONS)
assertEquals("get_balance", NwcMethod.GET_BALANCE)
assertEquals("get_info", NwcMethod.GET_INFO)
assertEquals("make_hold_invoice", NwcMethod.MAKE_HOLD_INVOICE)
assertEquals("cancel_hold_invoice", NwcMethod.CANCEL_HOLD_INVOICE)
assertEquals("settle_hold_invoice", NwcMethod.SETTLE_HOLD_INVOICE)
}
@Test
fun testNotificationTypeConstants() {
assertEquals("payment_received", NwcNotificationType.PAYMENT_RECEIVED)
assertEquals("payment_sent", NwcNotificationType.PAYMENT_SENT)
assertEquals("hold_invoice_accepted", NwcNotificationType.HOLD_INVOICE_ACCEPTED)
}
@Test
fun testErrorCodeValues() {
val codes = NwcErrorCode.entries
assertEquals(10, codes.size)
assertEquals(NwcErrorCode.RATE_LIMITED, NwcErrorCode.valueOf("RATE_LIMITED"))
assertEquals(NwcErrorCode.NOT_IMPLEMENTED, NwcErrorCode.valueOf("NOT_IMPLEMENTED"))
assertEquals(NwcErrorCode.INSUFFICIENT_BALANCE, NwcErrorCode.valueOf("INSUFFICIENT_BALANCE"))
assertEquals(NwcErrorCode.PAYMENT_FAILED, NwcErrorCode.valueOf("PAYMENT_FAILED"))
assertEquals(NwcErrorCode.QUOTA_EXCEEDED, NwcErrorCode.valueOf("QUOTA_EXCEEDED"))
assertEquals(NwcErrorCode.RESTRICTED, NwcErrorCode.valueOf("RESTRICTED"))
assertEquals(NwcErrorCode.UNAUTHORIZED, NwcErrorCode.valueOf("UNAUTHORIZED"))
assertEquals(NwcErrorCode.INTERNAL, NwcErrorCode.valueOf("INTERNAL"))
assertEquals(NwcErrorCode.UNSUPPORTED_ENCRYPTION, NwcErrorCode.valueOf("UNSUPPORTED_ENCRYPTION"))
assertEquals(NwcErrorCode.OTHER, NwcErrorCode.valueOf("OTHER"))
}
@Test
fun testNwcError() {
val error = NwcError(NwcErrorCode.UNAUTHORIZED, "not allowed")
assertEquals(NwcErrorCode.UNAUTHORIZED, error.code)
assertEquals("not allowed", error.message)
}
}
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class NwcNotificationEventTest {
@Test
fun testKindConstants() {
assertEquals(23197, NwcNotificationEvent.KIND)
assertEquals(23196, NwcNotificationEvent.LEGACY_KIND)
}
@Test
fun testIsContentEncoded() {
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = "b".repeat(64),
createdAt = 1234L,
tags = arrayOf(arrayOf("p", "c".repeat(64))),
content = "encrypted_content",
sig = "d".repeat(128),
)
assertTrue(event.isContentEncoded())
}
@Test
fun testClientPubKey() {
val clientPubKey = "c".repeat(64)
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = "b".repeat(64),
createdAt = 1234L,
tags = arrayOf(arrayOf("p", clientPubKey)),
content = "encrypted",
sig = "d".repeat(128),
)
assertEquals(clientPubKey, event.clientPubKey())
}
@Test
fun testClientPubKeyMissing() {
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = "b".repeat(64),
createdAt = 1234L,
tags = emptyArray(),
content = "encrypted",
sig = "d".repeat(128),
)
assertNull(event.clientPubKey())
}
@Test
fun testTalkingWithAsWalletService() {
val walletPubKey = "b".repeat(64)
val clientPubKey = "c".repeat(64)
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = walletPubKey,
createdAt = 1234L,
tags = arrayOf(arrayOf("p", clientPubKey)),
content = "encrypted",
sig = "d".repeat(128),
)
// Wallet service asking "who am I talking with?" -> client
assertEquals(clientPubKey, event.talkingWith(walletPubKey))
}
@Test
fun testTalkingWithAsClient() {
val walletPubKey = "b".repeat(64)
val clientPubKey = "c".repeat(64)
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = walletPubKey,
createdAt = 1234L,
tags = arrayOf(arrayOf("p", clientPubKey)),
content = "encrypted",
sig = "d".repeat(128),
)
// Client asking "who am I talking with?" -> wallet service (pubkey)
assertEquals(walletPubKey, event.talkingWith(clientPubKey))
}
@Test
fun testEventKindInFactory() {
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = "b".repeat(64),
createdAt = 1234L,
tags = emptyArray(),
content = "",
sig = "c".repeat(128),
)
assertEquals(23197, event.kind)
}
@Test
fun testCanDecryptReturnsFalseForUnrelatedSigner() {
val walletPubKey = "b".repeat(64)
val clientPubKey = "c".repeat(64)
val event =
NwcNotificationEvent(
id = "a".repeat(64),
pubKey = walletPubKey,
createdAt = 1234L,
tags = arrayOf(arrayOf("p", clientPubKey)),
content = "encrypted",
sig = "d".repeat(128),
)
// A signer that is neither the wallet nor the client shouldn't be able to decrypt
assertFalse(event.clientPubKey() == "e".repeat(64))
assertFalse(event.pubKey == "e".repeat(64))
}
}
@@ -0,0 +1,299 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class RequestTest {
// --- PayInvoice ---
@Test
fun testPayInvoiceCreate() {
val request = PayInvoiceMethod.create("lnbc50n1...")
assertEquals(NwcMethod.PAY_INVOICE, request.method)
assertEquals("lnbc50n1...", request.params?.invoice)
assertNull(request.params?.amount)
}
@Test
fun testPayInvoiceCreateWithAmount() {
val request = PayInvoiceMethod.create("lnbc50n1...", 1000L)
assertEquals(NwcMethod.PAY_INVOICE, request.method)
assertEquals("lnbc50n1...", request.params?.invoice)
assertEquals(1000L, request.params?.amount)
}
@Test
fun testPayInvoiceSerialization() {
val request = PayInvoiceMethod.create("lnbc50n1...")
val json = OptimizedJsonMapper.toJson(request)
assertTrue(json.contains("\"method\":\"pay_invoice\""))
assertTrue(json.contains("\"invoice\":\"lnbc50n1...\""))
}
@Test
fun testPayInvoiceDeserialization() {
val json = """{"method":"pay_invoice","params":{"invoice":"lnbc50n1..."}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<PayInvoiceMethod>(request)
assertEquals("lnbc50n1...", request.params?.invoice)
}
@Test
fun testPayInvoiceWithAmountDeserialization() {
val json = """{"method":"pay_invoice","params":{"invoice":"lnbc50n1...","amount":1000}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<PayInvoiceMethod>(request)
assertEquals("lnbc50n1...", request.params?.invoice)
assertEquals(1000L, request.params?.amount)
}
// --- PayKeysend ---
@Test
fun testPayKeysendCreate() {
val request = PayKeysendMethod.create(1000L, "abcdef1234567890")
assertEquals(NwcMethod.PAY_KEYSEND, request.method)
assertEquals(1000L, request.params?.amount)
assertEquals("abcdef1234567890", request.params?.pubkey)
assertNull(request.params?.preimage)
assertNull(request.params?.tlv_records)
}
@Test
fun testPayKeysendWithTlvRecords() {
val tlvRecords = listOf(TlvRecord(7629169L, "hex_value"))
val request = PayKeysendMethod.create(1000L, "pubkey123", "preimage123", tlvRecords)
assertEquals(1000L, request.params?.amount)
assertEquals("pubkey123", request.params?.pubkey)
assertEquals("preimage123", request.params?.preimage)
assertNotNull(request.params?.tlv_records)
assertEquals(1, request.params?.tlv_records?.size)
assertEquals(7629169L, request.params?.tlv_records?.first()?.type)
assertEquals("hex_value", request.params?.tlv_records?.first()?.value)
}
@Test
fun testPayKeysendSerialization() {
val request = PayKeysendMethod.create(1000L, "pubkey123")
val json = OptimizedJsonMapper.toJson(request)
assertTrue(json.contains("\"method\":\"pay_keysend\""))
assertTrue(json.contains("\"amount\":1000"))
assertTrue(json.contains("\"pubkey\":\"pubkey123\""))
}
@Test
fun testPayKeysendDeserialization() {
val json = """{"method":"pay_keysend","params":{"amount":1000,"pubkey":"pubkey123"}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<PayKeysendMethod>(request)
assertEquals(1000L, request.params?.amount)
assertEquals("pubkey123", request.params?.pubkey)
}
// --- MakeInvoice ---
@Test
fun testMakeInvoiceCreate() {
val request = MakeInvoiceMethod.create(5000L, "test payment", null, 3600L)
assertEquals(NwcMethod.MAKE_INVOICE, request.method)
assertEquals(5000L, request.params?.amount)
assertEquals("test payment", request.params?.description)
assertNull(request.params?.description_hash)
assertEquals(3600L, request.params?.expiry)
}
@Test
fun testMakeInvoiceSerialization() {
val request = MakeInvoiceMethod.create(5000L, "test")
val json = OptimizedJsonMapper.toJson(request)
assertTrue(json.contains("\"method\":\"make_invoice\""))
assertTrue(json.contains("\"amount\":5000"))
}
@Test
fun testMakeInvoiceDeserialization() {
val json = """{"method":"make_invoice","params":{"amount":5000,"description":"test","expiry":3600}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<MakeInvoiceMethod>(request)
assertEquals(5000L, request.params?.amount)
assertEquals("test", request.params?.description)
assertEquals(3600L, request.params?.expiry)
}
// --- LookupInvoice ---
@Test
fun testLookupInvoiceByHash() {
val request = LookupInvoiceMethod.createByHash("abc123")
assertEquals(NwcMethod.LOOKUP_INVOICE, request.method)
assertEquals("abc123", request.params?.payment_hash)
assertNull(request.params?.invoice)
}
@Test
fun testLookupInvoiceByInvoice() {
val request = LookupInvoiceMethod.createByInvoice("lnbc50n1...")
assertEquals(NwcMethod.LOOKUP_INVOICE, request.method)
assertNull(request.params?.payment_hash)
assertEquals("lnbc50n1...", request.params?.invoice)
}
@Test
fun testLookupInvoiceDeserialization() {
val json = """{"method":"lookup_invoice","params":{"payment_hash":"abc123"}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<LookupInvoiceMethod>(request)
assertEquals("abc123", request.params?.payment_hash)
}
// --- ListTransactions ---
@Test
fun testListTransactionsCreate() {
val request = ListTransactionsMethod.create(from = 1000L, until = 2000L, limit = 10, offset = 0, unpaid = false, type = "incoming")
assertEquals(NwcMethod.LIST_TRANSACTIONS, request.method)
assertEquals(1000L, request.params?.from)
assertEquals(2000L, request.params?.until)
assertEquals(10, request.params?.limit)
assertEquals(0, request.params?.offset)
assertEquals(false, request.params?.unpaid)
assertEquals("incoming", request.params?.type)
}
@Test
fun testListTransactionsDeserialization() {
val json = """{"method":"list_transactions","params":{"from":1000,"until":2000,"limit":10}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<ListTransactionsMethod>(request)
assertEquals(1000L, request.params?.from)
assertEquals(2000L, request.params?.until)
assertEquals(10, request.params?.limit)
}
// --- GetBalance ---
@Test
fun testGetBalanceCreate() {
val request = GetBalanceMethod.create()
assertEquals(NwcMethod.GET_BALANCE, request.method)
}
@Test
fun testGetBalanceSerialization() {
val request = GetBalanceMethod.create()
val json = OptimizedJsonMapper.toJson(request)
assertTrue(json.contains("\"method\":\"get_balance\""))
}
@Test
fun testGetBalanceDeserialization() {
val json = """{"method":"get_balance"}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<GetBalanceMethod>(request)
}
// --- GetInfo ---
@Test
fun testGetInfoCreate() {
val request = GetInfoMethod.create()
assertEquals(NwcMethod.GET_INFO, request.method)
}
@Test
fun testGetInfoDeserialization() {
val json = """{"method":"get_info"}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<GetInfoMethod>(request)
}
// --- MakeHoldInvoice ---
@Test
fun testMakeHoldInvoiceCreate() {
val request = MakeHoldInvoiceMethod.create(10000L, "payment_hash_abc", "hold invoice", null, 7200L, 144)
assertEquals(NwcMethod.MAKE_HOLD_INVOICE, request.method)
assertEquals(10000L, request.params?.amount)
assertEquals("payment_hash_abc", request.params?.payment_hash)
assertEquals("hold invoice", request.params?.description)
assertEquals(7200L, request.params?.expiry)
assertEquals(144, request.params?.min_cltv_expiry_delta)
}
@Test
fun testMakeHoldInvoiceDeserialization() {
val json = """{"method":"make_hold_invoice","params":{"amount":10000,"payment_hash":"abc","description":"test"}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<MakeHoldInvoiceMethod>(request)
assertEquals(10000L, request.params?.amount)
assertEquals("abc", request.params?.payment_hash)
}
// --- CancelHoldInvoice ---
@Test
fun testCancelHoldInvoiceCreate() {
val request = CancelHoldInvoiceMethod.create("payment_hash_abc")
assertEquals(NwcMethod.CANCEL_HOLD_INVOICE, request.method)
assertEquals("payment_hash_abc", request.params?.payment_hash)
}
@Test
fun testCancelHoldInvoiceDeserialization() {
val json = """{"method":"cancel_hold_invoice","params":{"payment_hash":"abc123"}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<CancelHoldInvoiceMethod>(request)
assertEquals("abc123", request.params?.payment_hash)
}
// --- SettleHoldInvoice ---
@Test
fun testSettleHoldInvoiceCreate() {
val request = SettleHoldInvoiceMethod.create("preimage_xyz")
assertEquals(NwcMethod.SETTLE_HOLD_INVOICE, request.method)
assertEquals("preimage_xyz", request.params?.preimage)
}
@Test
fun testSettleHoldInvoiceDeserialization() {
val json = """{"method":"settle_hold_invoice","params":{"preimage":"preimage_xyz"}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertIs<SettleHoldInvoiceMethod>(request)
assertEquals("preimage_xyz", request.params?.preimage)
}
// --- Unknown method ---
@Test
fun testUnknownMethodReturnsNull() {
val json = """{"method":"unknown_method","params":{}}"""
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
assertNull(request)
}
}
@@ -0,0 +1,268 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class ResponseTest {
// --- PayInvoice Success ---
@Test
fun testPayInvoiceSuccessDeserialization() {
val json = """{"result_type":"pay_invoice","result":{"preimage":"0123456789abcdef"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<PayInvoiceSuccessResponse>(response)
assertEquals("0123456789abcdef", response.result?.preimage)
}
@Test
fun testPayInvoiceSuccessWithFeesPaid() {
val json = """{"result_type":"pay_invoice","result":{"preimage":"abc","fees_paid":100}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<PayInvoiceSuccessResponse>(response)
assertEquals("abc", response.result?.preimage)
assertEquals(100L, response.result?.fees_paid)
}
@Test
fun testPayInvoiceSuccessGuessWithoutResultType() {
val json = """{"result":{"preimage":"0123456789abcdef"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<PayInvoiceSuccessResponse>(response)
assertEquals("0123456789abcdef", response.result?.preimage)
}
// --- PayInvoice Error ---
@Test
fun testPayInvoiceErrorDeserialization() {
val json = """{"result_type":"pay_invoice","error":{"code":"INSUFFICIENT_BALANCE","message":"Not enough funds"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<PayInvoiceErrorResponse>(response)
assertEquals(NwcErrorCode.INSUFFICIENT_BALANCE, response.error?.code)
assertEquals("Not enough funds", response.error?.message)
}
@Test
fun testPayInvoicePaymentFailedError() {
val json = """{"result_type":"pay_invoice","error":{"code":"PAYMENT_FAILED","message":"Route not found"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<PayInvoiceErrorResponse>(response)
assertEquals(NwcErrorCode.PAYMENT_FAILED, response.error?.code)
}
// --- PayKeysend Success ---
@Test
fun testPayKeysendSuccessDeserialization() {
val json = """{"result_type":"pay_keysend","result":{"preimage":"abc123","fees_paid":50}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<PayKeysendSuccessResponse>(response)
assertEquals("abc123", response.result?.preimage)
assertEquals(50L, response.result?.fees_paid)
}
// --- MakeInvoice Success ---
@Test
fun testMakeInvoiceSuccessDeserialization() {
val json =
"""{"result_type":"make_invoice","result":{"type":"incoming","invoice":"lnbc50n1...","description":"test","payment_hash":"abc","amount":5000,"fees_paid":0,"created_at":1693876497,"expires_at":1694876497}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<MakeInvoiceSuccessResponse>(response)
assertNotNull(response.result)
assertEquals("incoming", response.result?.type)
assertEquals("lnbc50n1...", response.result?.invoice)
assertEquals("test", response.result?.description)
assertEquals("abc", response.result?.payment_hash)
assertEquals(5000L, response.result?.amount)
assertEquals(1693876497L, response.result?.created_at)
assertEquals(1694876497L, response.result?.expires_at)
}
// --- LookupInvoice Success ---
@Test
fun testLookupInvoiceSuccessDeserialization() {
val json = """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash123","amount":1000,"settled_at":1694876497}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<LookupInvoiceSuccessResponse>(response)
assertNotNull(response.result)
assertEquals("incoming", response.result?.type)
assertEquals("settled", response.result?.state)
assertEquals("hash123", response.result?.payment_hash)
assertEquals(1000L, response.result?.amount)
assertEquals(1694876497L, response.result?.settled_at)
}
// --- ListTransactions Success ---
@Test
fun testListTransactionsSuccessDeserialization() {
val json =
"""{"result_type":"list_transactions","result":{"transactions":[{"type":"incoming","invoice":"lnbc1...","amount":100,"created_at":1000},{"type":"outgoing","invoice":"lnbc2...","amount":200,"created_at":2000}]}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<ListTransactionsSuccessResponse>(response)
assertNotNull(response.result?.transactions)
assertEquals(2, response.result?.transactions?.size)
assertEquals("incoming", response.result?.transactions?.get(0)?.type)
assertEquals(100L, response.result?.transactions?.get(0)?.amount)
assertEquals("outgoing", response.result?.transactions?.get(1)?.type)
assertEquals(200L, response.result?.transactions?.get(1)?.amount)
}
@Test
fun testListTransactionsEmptyResult() {
val json = """{"result_type":"list_transactions","result":{"transactions":[]}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<ListTransactionsSuccessResponse>(response)
assertNotNull(response.result?.transactions)
assertEquals(0, response.result?.transactions?.size)
}
// --- GetBalance Success ---
@Test
fun testGetBalanceSuccessDeserialization() {
val json = """{"result_type":"get_balance","result":{"balance":21000}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<GetBalanceSuccessResponse>(response)
assertEquals(21000L, response.result?.balance)
}
@Test
fun testGetBalanceZero() {
val json = """{"result_type":"get_balance","result":{"balance":0}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<GetBalanceSuccessResponse>(response)
assertEquals(0L, response.result?.balance)
}
// --- GetInfo Success ---
@Test
fun testGetInfoSuccessDeserialization() {
val json =
"""{"result_type":"get_info","result":{"alias":"MyNode","color":"#ff9900","pubkey":"abc123","network":"mainnet","block_height":800000,"block_hash":"hash","methods":["pay_invoice","get_balance"],"notifications":["payment_received"]}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<GetInfoSuccessResponse>(response)
assertNotNull(response.result)
assertEquals("MyNode", response.result?.alias)
assertEquals("#ff9900", response.result?.color)
assertEquals("abc123", response.result?.pubkey)
assertEquals("mainnet", response.result?.network)
assertEquals(800000L, response.result?.block_height)
assertEquals("hash", response.result?.block_hash)
assertEquals(listOf("pay_invoice", "get_balance"), response.result?.methods)
assertEquals(listOf("payment_received"), response.result?.notifications)
}
// --- MakeHoldInvoice Success ---
@Test
fun testMakeHoldInvoiceSuccessDeserialization() {
val json = """{"result_type":"make_hold_invoice","result":{"type":"incoming","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"expires_at":2000}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<MakeHoldInvoiceSuccessResponse>(response)
assertNotNull(response.result)
assertEquals("lnbc...", response.result?.invoice)
assertEquals("hash", response.result?.payment_hash)
}
// --- CancelHoldInvoice Success ---
@Test
fun testCancelHoldInvoiceSuccessDeserialization() {
val json = """{"result_type":"cancel_hold_invoice","result":{}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<CancelHoldInvoiceSuccessResponse>(response)
}
// --- SettleHoldInvoice Success ---
@Test
fun testSettleHoldInvoiceSuccessDeserialization() {
val json = """{"result_type":"settle_hold_invoice","result":{}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<SettleHoldInvoiceSuccessResponse>(response)
}
// --- Generic Error Response ---
@Test
fun testGenericErrorForGetBalance() {
val json = """{"result_type":"get_balance","error":{"code":"UNAUTHORIZED","message":"No wallet connected"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<NwcErrorResponse>(response)
assertEquals("get_balance", response.resultType)
assertEquals(NwcErrorCode.UNAUTHORIZED, response.error?.code)
assertEquals("No wallet connected", response.error?.message)
}
@Test
fun testGenericErrorForGetInfo() {
val json = """{"result_type":"get_info","error":{"code":"NOT_IMPLEMENTED","message":"Not supported"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<NwcErrorResponse>(response)
assertEquals("get_info", response.resultType)
assertEquals(NwcErrorCode.NOT_IMPLEMENTED, response.error?.code)
}
@Test
fun testGenericErrorForMakeInvoice() {
val json = """{"result_type":"make_invoice","error":{"code":"QUOTA_EXCEEDED","message":"Spending limit reached"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<NwcErrorResponse>(response)
assertEquals(NwcErrorCode.QUOTA_EXCEEDED, response.error?.code)
}
@Test
fun testGenericErrorRateLimited() {
val json = """{"result_type":"pay_keysend","error":{"code":"RATE_LIMITED","message":"Too many requests"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
assertIs<NwcErrorResponse>(response)
assertEquals(NwcErrorCode.RATE_LIMITED, response.error?.code)
}
@Test
fun testGenericErrorUnsupportedEncryption() {
val json = """{"result_type":"pay_invoice","error":{"code":"UNSUPPORTED_ENCRYPTION","message":"Use nip44"}}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
// pay_invoice errors go to PayInvoiceErrorResponse for backward compat
assertIs<PayInvoiceErrorResponse>(response)
}
// --- Null/missing result ---
@Test
fun testResponseWithNoResultOrError() {
val json = """{"result_type":"pay_invoice"}"""
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
// Should still deserialize since result_type is present
assertIs<PayInvoiceSuccessResponse>(response)
assertNull(response.result)
}
}
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.tags.EncryptionTag
import com.vitorpamplona.quartz.nip47WalletConnect.tags.NotificationsTag
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class TagsTest {
// --- EncryptionTag ---
@Test
fun testEncryptionTagParse() {
val tag = arrayOf("encryption", "nip44_v2", "nip04")
val result = EncryptionTag.parse(tag)
assertNotNull(result)
assertEquals(listOf("nip44_v2", "nip04"), result)
}
@Test
fun testEncryptionTagParseSingleScheme() {
val tag = arrayOf("encryption", "nip44_v2")
val result = EncryptionTag.parse(tag)
assertNotNull(result)
assertEquals(listOf("nip44_v2"), result)
}
@Test
fun testEncryptionTagParseWrongTagName() {
val tag = arrayOf("other", "nip44_v2")
val result = EncryptionTag.parse(tag)
assertNull(result)
}
@Test
fun testEncryptionTagParseTooShort() {
val tag = arrayOf("encryption")
val result = EncryptionTag.parse(tag)
assertNull(result)
}
@Test
fun testEncryptionTagParseEmptyValue() {
val tag = arrayOf("encryption", "")
val result = EncryptionTag.parse(tag)
assertNull(result)
}
@Test
fun testEncryptionTagAssemble() {
val tag = EncryptionTag.assemble(listOf("nip44_v2", "nip04"))
assertEquals("encryption", tag[0])
assertEquals("nip44_v2", tag[1])
assertEquals("nip04", tag[2])
assertEquals(3, tag.size)
}
@Test
fun testEncryptionTagIsTag() {
assertTrue(EncryptionTag.isTag(arrayOf("encryption", "nip44_v2")))
assertFalse(EncryptionTag.isTag(arrayOf("other", "nip44_v2")))
assertFalse(EncryptionTag.isTag(arrayOf("encryption")))
assertFalse(EncryptionTag.isTag(arrayOf("encryption", "")))
}
// --- NotificationsTag ---
@Test
fun testNotificationsTagParse() {
val tag = arrayOf("notifications", "payment_received", "payment_sent")
val result = NotificationsTag.parse(tag)
assertNotNull(result)
assertEquals(listOf("payment_received", "payment_sent"), result)
}
@Test
fun testNotificationsTagParseSingleType() {
val tag = arrayOf("notifications", "payment_received")
val result = NotificationsTag.parse(tag)
assertNotNull(result)
assertEquals(listOf("payment_received"), result)
}
@Test
fun testNotificationsTagParseWrongTagName() {
val tag = arrayOf("other", "payment_received")
val result = NotificationsTag.parse(tag)
assertNull(result)
}
@Test
fun testNotificationsTagParseTooShort() {
val tag = arrayOf("notifications")
val result = NotificationsTag.parse(tag)
assertNull(result)
}
@Test
fun testNotificationsTagAssemble() {
val tag = NotificationsTag.assemble(listOf("payment_received", "payment_sent", "hold_invoice_accepted"))
assertEquals("notifications", tag[0])
assertEquals("payment_received", tag[1])
assertEquals("payment_sent", tag[2])
assertEquals("hold_invoice_accepted", tag[3])
assertEquals(4, tag.size)
}
@Test
fun testNotificationsTagIsTag() {
assertTrue(NotificationsTag.isTag(arrayOf("notifications", "payment_received")))
assertFalse(NotificationsTag.isTag(arrayOf("other", "payment_received")))
assertFalse(NotificationsTag.isTag(arrayOf("notifications")))
assertFalse(NotificationsTag.isTag(arrayOf("notifications", "")))
}
}