feat: add Alby JS SDK client interop improvements
- Add NwcBudgetRenewal constants (daily/weekly/monthly/yearly/never) - Add case-insensitive NwcTransactionState helpers (isSettled, isPending, isFailed, isAccepted) for JS SDK lowercase state interop - Add URI test using Alby JS SDK test vector (69effe7b... pubkey) - Add JS SDK interop tests: lowercase transaction states in responses, notifications, and list_transactions; empty get_budget response; all budget renewal periods; structured transaction metadata; full 13-method get_info response https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
This commit is contained in:
+16
@@ -30,6 +30,22 @@ object NwcTransactionState {
|
||||
const val SETTLED = "SETTLED"
|
||||
const val FAILED = "FAILED"
|
||||
const val ACCEPTED = "ACCEPTED"
|
||||
|
||||
fun isSettled(state: String?) = state.equals(SETTLED, ignoreCase = true)
|
||||
|
||||
fun isPending(state: String?) = state.equals(PENDING, ignoreCase = true)
|
||||
|
||||
fun isFailed(state: String?) = state.equals(FAILED, ignoreCase = true)
|
||||
|
||||
fun isAccepted(state: String?) = state.equals(ACCEPTED, ignoreCase = true)
|
||||
}
|
||||
|
||||
object NwcBudgetRenewal {
|
||||
const val DAILY = "daily"
|
||||
const val WEEKLY = "weekly"
|
||||
const val MONTHLY = "monthly"
|
||||
const val YEARLY = "yearly"
|
||||
const val NEVER = "never"
|
||||
}
|
||||
|
||||
class NwcTransaction(
|
||||
|
||||
+99
-2
@@ -26,10 +26,12 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Interoperability tests using JSON structures matching Alby Hub's NIP-47 implementation.
|
||||
* These tests verify that Amethyst can correctly parse responses from Alby Hub wallets.
|
||||
* Interoperability tests using JSON structures matching Alby Hub (server)
|
||||
* and Alby JS SDK (client) NIP-47 implementations.
|
||||
* These tests verify that Amethyst can correctly parse responses from Alby wallets.
|
||||
*/
|
||||
class AlbyInteropTest {
|
||||
// --- Alby Hub pay_invoice response format ---
|
||||
@@ -404,4 +406,99 @@ class AlbyInteropTest {
|
||||
assertEquals(144L, txn.settle_deadline)
|
||||
assertEquals(NwcTransactionState.PENDING, txn.state)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Alby JS SDK (client) interop tests
|
||||
// The JS SDK uses lowercase transaction states while Hub uses uppercase.
|
||||
// Both formats must be handled correctly.
|
||||
// ===================================================================
|
||||
|
||||
@Test
|
||||
fun testJsSdkLowercaseSettledState() {
|
||||
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)
|
||||
assertEquals("settled", response.result?.state)
|
||||
assertTrue(NwcTransactionState.isSettled(response.result?.state))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkLowercasePendingState() {
|
||||
val json =
|
||||
"""{"result_type":"make_invoice","result":{"type":"incoming","state":"pending","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<MakeInvoiceSuccessResponse>(response)
|
||||
assertEquals("pending", response.result?.state)
|
||||
assertTrue(NwcTransactionState.isPending(response.result?.state))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkLowercaseStatesInListTransactions() {
|
||||
val json =
|
||||
"""{"result_type":"list_transactions","result":{"transactions":[{"type":"incoming","state":"settled","amount":1000,"created_at":1000},{"type":"outgoing","state":"failed","amount":2000,"created_at":2000},{"type":"incoming","state":"accepted","amount":3000,"created_at":3000}],"total_count":3}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<ListTransactionsSuccessResponse>(response)
|
||||
val txns = response.result?.transactions
|
||||
assertNotNull(txns)
|
||||
assertEquals(3, txns.size)
|
||||
assertTrue(NwcTransactionState.isSettled(txns[0].state))
|
||||
assertTrue(NwcTransactionState.isFailed(txns[1].state))
|
||||
assertTrue(NwcTransactionState.isAccepted(txns[2].state))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkLowercaseStatesInNotification() {
|
||||
val json =
|
||||
"""{"notification_type":"payment_received","notification":{"type":"incoming","state":"settled","invoice":"lnbc...","amount":5000,"created_at":1000,"settled_at":2000}}"""
|
||||
val notification = OptimizedJsonMapper.fromJsonTo<Notification>(json)
|
||||
assertIs<PaymentReceivedNotification>(notification)
|
||||
assertTrue(NwcTransactionState.isSettled(notification.notification?.state))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkEmptyGetBudgetResponse() {
|
||||
// JS SDK allows get_budget to return empty object when no budget is set
|
||||
val json = """{"result_type":"get_budget","result":{}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<GetBudgetSuccessResponse>(response)
|
||||
assertNull(response.result?.used_budget)
|
||||
assertNull(response.result?.total_budget)
|
||||
assertNull(response.result?.renews_at)
|
||||
assertNull(response.result?.renewal_period)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkGetBudgetWithAllRenewalPeriods() {
|
||||
for (period in listOf("daily", "weekly", "monthly", "yearly", "never")) {
|
||||
val json = """{"result_type":"get_budget","result":{"used_budget":0,"total_budget":100000,"renewal_period":"$period"}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<GetBudgetSuccessResponse>(response)
|
||||
assertEquals(period, response.result?.renewal_period)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkTransactionWithMetadata() {
|
||||
// JS SDK supports structured metadata with comment, payer_data, nostr fields
|
||||
val json =
|
||||
"""{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000,"metadata":{"comment":"Thanks!","payer_data":{"name":"Alice","pubkey":"abc123"},"nostr":{"pubkey":"npub1...","tags":[["p","def456"]]}}}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<LookupInvoiceSuccessResponse>(response)
|
||||
assertNotNull(response.result?.metadata)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJsSdkGetInfoWithAllMethods() {
|
||||
// JS SDK advertises all 13 single methods + notifications
|
||||
val json =
|
||||
"""{"result_type":"get_info","result":{"alias":"TestNode","methods":["get_info","get_balance","get_budget","make_invoice","pay_invoice","pay_keysend","lookup_invoice","list_transactions","sign_message","create_connection","make_hold_invoice","settle_hold_invoice","cancel_hold_invoice"],"notifications":["payment_received","payment_sent","hold_invoice_accepted"]}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<GetInfoSuccessResponse>(response)
|
||||
assertEquals(13, response.result?.methods?.size)
|
||||
assertEquals(3, response.result?.notifications?.size)
|
||||
assertTrue(response.result?.methods?.contains(NwcMethod.GET_BUDGET) == true)
|
||||
assertTrue(response.result?.methods?.contains(NwcMethod.SIGN_MESSAGE) == true)
|
||||
assertTrue(response.result?.methods?.contains(NwcMethod.CREATE_CONNECTION) == true)
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -93,6 +93,20 @@ class Nip47WalletConnectTest {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Alby JS SDK URI test vector ---
|
||||
|
||||
@Test
|
||||
fun testParseAlbyJsSdkUri() {
|
||||
// Test vector from @getalby/js-sdk NWCClient.test.ts
|
||||
val uri =
|
||||
"nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss%3A%2F%2Frelay.getalby.com%2Fv1&secret=e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b&lud16=hello%40getalby.com"
|
||||
val parsed = Nip47WalletConnect.parse(uri)
|
||||
|
||||
assertEquals("69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", parsed.pubKeyHex)
|
||||
assertEquals("e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b", parsed.secret)
|
||||
assertEquals("hello@getalby.com", parsed.lud16)
|
||||
}
|
||||
|
||||
// --- Nip47URI serialization ---
|
||||
|
||||
@Test
|
||||
|
||||
+26
@@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NwcMethodTest {
|
||||
@Test
|
||||
@@ -81,6 +83,30 @@ class NwcMethodTest {
|
||||
assertEquals("ACCEPTED", NwcTransactionState.ACCEPTED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTransactionStateCaseInsensitive() {
|
||||
// Alby Hub uses uppercase, Alby JS SDK uses lowercase
|
||||
assertTrue(NwcTransactionState.isSettled("SETTLED"))
|
||||
assertTrue(NwcTransactionState.isSettled("settled"))
|
||||
assertTrue(NwcTransactionState.isPending("PENDING"))
|
||||
assertTrue(NwcTransactionState.isPending("pending"))
|
||||
assertTrue(NwcTransactionState.isFailed("FAILED"))
|
||||
assertTrue(NwcTransactionState.isFailed("failed"))
|
||||
assertTrue(NwcTransactionState.isAccepted("ACCEPTED"))
|
||||
assertTrue(NwcTransactionState.isAccepted("accepted"))
|
||||
assertFalse(NwcTransactionState.isSettled("pending"))
|
||||
assertFalse(NwcTransactionState.isSettled(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBudgetRenewalConstants() {
|
||||
assertEquals("daily", NwcBudgetRenewal.DAILY)
|
||||
assertEquals("weekly", NwcBudgetRenewal.WEEKLY)
|
||||
assertEquals("monthly", NwcBudgetRenewal.MONTHLY)
|
||||
assertEquals("yearly", NwcBudgetRenewal.YEARLY)
|
||||
assertEquals("never", NwcBudgetRenewal.NEVER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNwcError() {
|
||||
val error = NwcError(NwcErrorCode.UNAUTHORIZED, "not allowed")
|
||||
|
||||
Reference in New Issue
Block a user