mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(nwc): add pay/receive methods for BOLT12 (nostr-wallet-connect/nwc#2)
Adds the generalized `pay` (BIP321 payment instruction, incl. BOLT12 `lno=`) and `receive` NIP-47 methods, plus the UNSUPPORTED_PAYMENT_INSTRUCTION / UNSUPPORTED_NETWORK error codes. The `pay` result carries `payer_proof` (lnp1…) — the proof a kind:9736 BOLT12 zap needs. Wired through both serialization backends (kotlinx + Jackson) with round-trip tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
This commit is contained in:
+56
@@ -40,6 +40,10 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceParams
|
||||
@@ -85,6 +89,14 @@ object Nip47RequestKSerializer : KSerializer<Request> {
|
||||
value.params?.let { put("params", serializePayInvoiceParams(it)) }
|
||||
}
|
||||
|
||||
is PayMethod -> {
|
||||
value.params?.let { put("params", serializePayParams(it)) }
|
||||
}
|
||||
|
||||
is ReceiveMethod -> {
|
||||
value.params?.let { put("params", serializeReceiveParams(it)) }
|
||||
}
|
||||
|
||||
is PayKeysendMethod -> {
|
||||
value.params?.let { put("params", serializePayKeysendParams(it)) }
|
||||
}
|
||||
@@ -138,6 +150,21 @@ object Nip47RequestKSerializer : KSerializer<Request> {
|
||||
params.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) }
|
||||
}
|
||||
|
||||
private fun serializePayParams(params: PayParams): JsonObject =
|
||||
buildJsonObject {
|
||||
params.payment?.let { put("payment", it) }
|
||||
params.amount?.let { put("amount", it) }
|
||||
params.payer_note?.let { put("payer_note", it) }
|
||||
params.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) }
|
||||
}
|
||||
|
||||
private fun serializeReceiveParams(params: ReceiveParams): JsonObject =
|
||||
buildJsonObject {
|
||||
params.amount?.let { put("amount", it) }
|
||||
params.description?.let { put("description", it) }
|
||||
params.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) }
|
||||
}
|
||||
|
||||
private fun serializePayKeysendParams(params: PayKeysendParams): JsonObject =
|
||||
buildJsonObject {
|
||||
params.amount?.let { put("amount", it) }
|
||||
@@ -236,6 +263,8 @@ object Nip47RequestKSerializer : KSerializer<Request> {
|
||||
|
||||
return when (method) {
|
||||
NwcMethod.PAY_INVOICE -> parsePayInvoice(jsonObject)
|
||||
NwcMethod.PAY -> parsePay(jsonObject)
|
||||
NwcMethod.RECEIVE -> parseReceive(jsonObject)
|
||||
NwcMethod.PAY_KEYSEND -> parsePayKeysend(jsonObject)
|
||||
NwcMethod.MAKE_INVOICE -> parseMakeInvoice(jsonObject)
|
||||
NwcMethod.LOOKUP_INVOICE -> parseLookupInvoice(jsonObject)
|
||||
@@ -265,6 +294,33 @@ object Nip47RequestKSerializer : KSerializer<Request> {
|
||||
)
|
||||
}
|
||||
|
||||
private fun parsePay(json: JsonObject): PayMethod {
|
||||
val params = json["params"]?.jsonObject
|
||||
return PayMethod(
|
||||
params?.let {
|
||||
PayParams(
|
||||
payment = it["payment"]?.jsonPrimitive?.content,
|
||||
amount = it["amount"]?.jsonPrimitive?.longOrNull,
|
||||
payer_note = it["payer_note"]?.jsonPrimitive?.content,
|
||||
metadata = it["metadata"]?.jsonObject?.toAnyMap(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseReceive(json: JsonObject): ReceiveMethod {
|
||||
val params = json["params"]?.jsonObject
|
||||
return ReceiveMethod(
|
||||
params?.let {
|
||||
ReceiveParams(
|
||||
amount = it["amount"]?.jsonPrimitive?.longOrNull,
|
||||
description = it["description"]?.jsonPrimitive?.content,
|
||||
metadata = it["metadata"]?.jsonObject?.toAnyMap(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parsePayKeysend(json: JsonObject): PayKeysendMethod {
|
||||
val params = json["params"]?.jsonObject
|
||||
return PayKeysendMethod(
|
||||
|
||||
+74
@@ -37,6 +37,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
@@ -81,6 +83,14 @@ object Nip47ResponseKSerializer : KSerializer<Response> {
|
||||
value.result?.let { put("result", serializePayInvoiceResult(it)) }
|
||||
}
|
||||
|
||||
is PaySuccessResponse -> {
|
||||
value.result?.let { put("result", serializePayResult(it)) }
|
||||
}
|
||||
|
||||
is ReceiveSuccessResponse -> {
|
||||
value.result?.let { put("result", serializeReceiveResult(it)) }
|
||||
}
|
||||
|
||||
is PayInvoiceErrorResponse -> {
|
||||
value.error?.let { put("error", serializePayInvoiceErrorParams(it)) }
|
||||
}
|
||||
@@ -155,6 +165,28 @@ object Nip47ResponseKSerializer : KSerializer<Response> {
|
||||
error.message?.let { put("message", it) }
|
||||
}
|
||||
|
||||
private fun serializePayResult(result: PaySuccessResponse.PayResult): JsonObject =
|
||||
buildJsonObject {
|
||||
result.transaction_id?.let { put("transaction_id", it) }
|
||||
result.state?.let { put("state", it) }
|
||||
result.instruction_type?.let { put("instruction_type", it) }
|
||||
result.amount?.let { put("amount", it) }
|
||||
result.fees_paid?.let { put("fees_paid", it) }
|
||||
result.payment_hash?.let { put("payment_hash", it) }
|
||||
result.preimage?.let { put("preimage", it) }
|
||||
result.payer_proof?.let { put("payer_proof", it) }
|
||||
result.txid?.let { put("txid", it) }
|
||||
result.failure_reason?.let { put("failure_reason", it) }
|
||||
result.created_at?.let { put("created_at", it) }
|
||||
result.settled_at?.let { put("settled_at", it) }
|
||||
}
|
||||
|
||||
private fun serializeReceiveResult(result: ReceiveSuccessResponse.ReceiveResult): JsonObject =
|
||||
buildJsonObject {
|
||||
result.bip321?.let { put("bip321", it) }
|
||||
result.transaction_id?.let { put("transaction_id", it) }
|
||||
}
|
||||
|
||||
private fun serializePayKeysendResult(result: PayKeysendSuccessResponse.PayKeysendResult): JsonObject =
|
||||
buildJsonObject {
|
||||
result.preimage?.let { put("preimage", it) }
|
||||
@@ -242,6 +274,14 @@ object Nip47ResponseKSerializer : KSerializer<Response> {
|
||||
parsePayInvoiceSuccess(jsonObject)
|
||||
}
|
||||
|
||||
NwcMethod.PAY -> {
|
||||
parsePaySuccess(jsonObject)
|
||||
}
|
||||
|
||||
NwcMethod.RECEIVE -> {
|
||||
parseReceiveSuccess(jsonObject)
|
||||
}
|
||||
|
||||
NwcMethod.PAY_KEYSEND -> {
|
||||
parsePayKeysendSuccess(jsonObject)
|
||||
}
|
||||
@@ -387,6 +427,40 @@ object Nip47ResponseKSerializer : KSerializer<Response> {
|
||||
)
|
||||
}
|
||||
|
||||
private fun parsePaySuccess(json: JsonObject): PaySuccessResponse {
|
||||
val result = json["result"]?.jsonObject
|
||||
return PaySuccessResponse(
|
||||
result?.let {
|
||||
PaySuccessResponse.PayResult(
|
||||
transaction_id = it["transaction_id"]?.jsonPrimitive?.content,
|
||||
state = it["state"]?.jsonPrimitive?.content,
|
||||
instruction_type = it["instruction_type"]?.jsonPrimitive?.content,
|
||||
amount = it["amount"]?.jsonPrimitive?.longOrNull,
|
||||
fees_paid = it["fees_paid"]?.jsonPrimitive?.longOrNull,
|
||||
payment_hash = it["payment_hash"]?.jsonPrimitive?.content,
|
||||
preimage = it["preimage"]?.jsonPrimitive?.content,
|
||||
payer_proof = it["payer_proof"]?.jsonPrimitive?.content,
|
||||
txid = it["txid"]?.jsonPrimitive?.content,
|
||||
failure_reason = it["failure_reason"]?.jsonPrimitive?.content,
|
||||
created_at = it["created_at"]?.jsonPrimitive?.longOrNull,
|
||||
settled_at = it["settled_at"]?.jsonPrimitive?.longOrNull,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseReceiveSuccess(json: JsonObject): ReceiveSuccessResponse {
|
||||
val result = json["result"]?.jsonObject
|
||||
return ReceiveSuccessResponse(
|
||||
result?.let {
|
||||
ReceiveSuccessResponse.ReceiveResult(
|
||||
bip321 = it["bip321"]?.jsonPrimitive?.content,
|
||||
transaction_id = it["transaction_id"]?.jsonPrimitive?.content,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parsePayKeysendSuccess(json: JsonObject): PayKeysendSuccessResponse {
|
||||
val result = json["result"]?.jsonObject
|
||||
return PayKeysendSuccessResponse(
|
||||
|
||||
+4
@@ -33,6 +33,10 @@ enum class NwcErrorCode {
|
||||
BAD_REQUEST,
|
||||
NOT_FOUND,
|
||||
EXPIRED,
|
||||
|
||||
// nostr-wallet-connect/nwc#2 — pay/receive payment-instruction errors.
|
||||
UNSUPPORTED_PAYMENT_INSTRUCTION,
|
||||
UNSUPPORTED_NETWORK,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -34,4 +34,8 @@ object NwcMethod {
|
||||
const val MAKE_HOLD_INVOICE = "make_hold_invoice"
|
||||
const val CANCEL_HOLD_INVOICE = "cancel_hold_invoice"
|
||||
const val SETTLE_HOLD_INVOICE = "settle_hold_invoice"
|
||||
|
||||
// nostr-wallet-connect/nwc#2 — generalized payment instructions (BOLT12/BIP321).
|
||||
const val PAY = "pay"
|
||||
const val RECEIVE = "receive"
|
||||
}
|
||||
|
||||
+42
@@ -47,6 +47,48 @@ class PayInvoiceMethod(
|
||||
}
|
||||
}
|
||||
|
||||
// pay (nostr-wallet-connect/nwc#2) — settle any BIP321 payment instruction
|
||||
// (bolt11 `lightning=`, BOLT12 `lno=`, or on-chain). `payment` is the BIP321 URI;
|
||||
// `amount` (msats) is required only when the instruction has no amount; `payer_note`
|
||||
// is delivered to the payee when the instruction supports payer messages (for a
|
||||
// BOLT12 zap this carries `nostr:nipXX:<intent-id>`).
|
||||
class PayParams(
|
||||
var payment: String? = null,
|
||||
var amount: Long? = null,
|
||||
var payer_note: String? = null,
|
||||
var metadata: Map<String, Any?>? = null,
|
||||
)
|
||||
|
||||
class PayMethod(
|
||||
var params: PayParams? = null,
|
||||
) : Request(NwcMethod.PAY) {
|
||||
companion object {
|
||||
fun create(
|
||||
payment: String,
|
||||
amount: Long? = null,
|
||||
payerNote: String? = null,
|
||||
): PayMethod = PayMethod(PayParams(payment, amount, payerNote))
|
||||
}
|
||||
}
|
||||
|
||||
// receive (nostr-wallet-connect/nwc#2) — mint a payment instruction to be paid.
|
||||
class ReceiveParams(
|
||||
var amount: Long? = null,
|
||||
var description: String? = null,
|
||||
var metadata: Map<String, Any?>? = null,
|
||||
)
|
||||
|
||||
class ReceiveMethod(
|
||||
var params: ReceiveParams? = null,
|
||||
) : Request(NwcMethod.RECEIVE) {
|
||||
companion object {
|
||||
fun create(
|
||||
amount: Long? = null,
|
||||
description: String? = null,
|
||||
): ReceiveMethod = ReceiveMethod(ReceiveParams(amount, description))
|
||||
}
|
||||
}
|
||||
|
||||
// pay_keysend
|
||||
class PayKeysendParams(
|
||||
var amount: Long? = null,
|
||||
|
||||
+32
@@ -68,6 +68,38 @@ class PayInvoiceErrorResponse(
|
||||
override fun errorMessage() = error?.message
|
||||
}
|
||||
|
||||
// pay success response (nostr-wallet-connect/nwc#2). For a settled BOLT12 payment
|
||||
// `payer_proof` carries the `lnp1...` proof — the input a kind:9736 BOLT12 zap needs.
|
||||
// It is best-effort ("optional if unavailable"), so callers must tolerate its absence.
|
||||
class PaySuccessResponse(
|
||||
val result: PayResult? = null,
|
||||
) : Response(NwcMethod.PAY) {
|
||||
class PayResult(
|
||||
val transaction_id: String? = null,
|
||||
val state: String? = null,
|
||||
val instruction_type: String? = null,
|
||||
val amount: Long? = null,
|
||||
val fees_paid: Long? = null,
|
||||
val payment_hash: String? = null,
|
||||
val preimage: String? = null,
|
||||
val payer_proof: String? = null,
|
||||
val txid: String? = null,
|
||||
val failure_reason: String? = null,
|
||||
val created_at: Long? = null,
|
||||
val settled_at: Long? = null,
|
||||
)
|
||||
}
|
||||
|
||||
// receive success response (nostr-wallet-connect/nwc#2).
|
||||
class ReceiveSuccessResponse(
|
||||
val result: ReceiveResult? = null,
|
||||
) : Response(NwcMethod.RECEIVE) {
|
||||
class ReceiveResult(
|
||||
val bip321: String? = null,
|
||||
val transaction_id: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
// pay_keysend success response
|
||||
class PayKeysendSuccessResponse(
|
||||
val result: PayKeysendResult? = null,
|
||||
|
||||
+5
-1
@@ -48,6 +48,8 @@ class NwcMethodTest {
|
||||
assertEquals("make_hold_invoice", NwcMethod.MAKE_HOLD_INVOICE)
|
||||
assertEquals("cancel_hold_invoice", NwcMethod.CANCEL_HOLD_INVOICE)
|
||||
assertEquals("settle_hold_invoice", NwcMethod.SETTLE_HOLD_INVOICE)
|
||||
assertEquals("pay", NwcMethod.PAY)
|
||||
assertEquals("receive", NwcMethod.RECEIVE)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -60,7 +62,7 @@ class NwcMethodTest {
|
||||
@Test
|
||||
fun testErrorCodeValues() {
|
||||
val codes = NwcErrorCode.entries
|
||||
assertEquals(13, codes.size)
|
||||
assertEquals(15, 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"))
|
||||
@@ -73,6 +75,8 @@ class NwcMethodTest {
|
||||
assertEquals(NwcErrorCode.BAD_REQUEST, NwcErrorCode.valueOf("BAD_REQUEST"))
|
||||
assertEquals(NwcErrorCode.NOT_FOUND, NwcErrorCode.valueOf("NOT_FOUND"))
|
||||
assertEquals(NwcErrorCode.EXPIRED, NwcErrorCode.valueOf("EXPIRED"))
|
||||
assertEquals(NwcErrorCode.UNSUPPORTED_PAYMENT_INSTRUCTION, NwcErrorCode.valueOf("UNSUPPORTED_PAYMENT_INSTRUCTION"))
|
||||
assertEquals(NwcErrorCode.UNSUPPORTED_NETWORK, NwcErrorCode.valueOf("UNSUPPORTED_NETWORK"))
|
||||
assertEquals(NwcErrorCode.OTHER, NwcErrorCode.valueOf("OTHER"))
|
||||
}
|
||||
|
||||
|
||||
+63
@@ -33,6 +33,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
@@ -89,6 +91,67 @@ class RequestTest {
|
||||
assertEquals(1000L, request.params?.amount)
|
||||
}
|
||||
|
||||
// --- Pay (nwc#2 BOLT12/BIP321) ---
|
||||
|
||||
@Test
|
||||
fun testPayCreateWithOfferAndPayerNote() {
|
||||
val request = PayMethod.create("bitcoin:?lno=lno1abc", amount = 21_000L, payerNote = "nostr:nipXX:deadbeef")
|
||||
assertEquals(NwcMethod.PAY, request.method)
|
||||
assertEquals("bitcoin:?lno=lno1abc", request.params?.payment)
|
||||
assertEquals(21_000L, request.params?.amount)
|
||||
assertEquals("nostr:nipXX:deadbeef", request.params?.payer_note)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPaySerialization() {
|
||||
val request = PayMethod.create("bitcoin:?lno=lno1abc", amount = 21_000L, payerNote = "nostr:nipXX:deadbeef")
|
||||
val json = OptimizedJsonMapper.toJson(request)
|
||||
assertTrue(json.contains("\"method\":\"pay\""))
|
||||
assertTrue(json.contains("\"payment\":\"bitcoin:?lno=lno1abc\""))
|
||||
assertTrue(json.contains("\"amount\":21000"))
|
||||
assertTrue(json.contains("\"payer_note\":\"nostr:nipXX:deadbeef\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPayDeserialization() {
|
||||
val json = """{"method":"pay","params":{"payment":"bitcoin:?lno=lno1abc","amount":21000,"payer_note":"nostr:nipXX:deadbeef"}}"""
|
||||
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
|
||||
assertIs<PayMethod>(request)
|
||||
assertEquals("bitcoin:?lno=lno1abc", request.params?.payment)
|
||||
assertEquals(21_000L, request.params?.amount)
|
||||
assertEquals("nostr:nipXX:deadbeef", request.params?.payer_note)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPayAmountlessDeserialization() {
|
||||
val json = """{"method":"pay","params":{"payment":"bitcoin:?lno=lno1abc"}}"""
|
||||
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
|
||||
assertIs<PayMethod>(request)
|
||||
assertEquals("bitcoin:?lno=lno1abc", request.params?.payment)
|
||||
assertNull(request.params?.amount)
|
||||
assertNull(request.params?.payer_note)
|
||||
}
|
||||
|
||||
// --- Receive (nwc#2) ---
|
||||
|
||||
@Test
|
||||
fun testReceiveSerialization() {
|
||||
val request = ReceiveMethod.create(amount = 21_000L, description = "tip jar")
|
||||
val json = OptimizedJsonMapper.toJson(request)
|
||||
assertTrue(json.contains("\"method\":\"receive\""))
|
||||
assertTrue(json.contains("\"amount\":21000"))
|
||||
assertTrue(json.contains("\"description\":\"tip jar\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReceiveDeserialization() {
|
||||
val json = """{"method":"receive","params":{"amount":21000,"description":"tip jar"}}"""
|
||||
val request = OptimizedJsonMapper.fromJsonTo<Request>(json)
|
||||
assertIs<ReceiveMethod>(request)
|
||||
assertEquals(21_000L, request.params?.amount)
|
||||
assertEquals("tip jar", request.params?.description)
|
||||
}
|
||||
|
||||
// --- PayKeysend ---
|
||||
|
||||
@Test
|
||||
|
||||
+49
@@ -35,6 +35,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
@@ -91,6 +93,53 @@ class ResponseTest {
|
||||
assertEquals(NwcErrorCode.PAYMENT_FAILED, response.error?.code)
|
||||
}
|
||||
|
||||
// --- Pay Success (nwc#2 BOLT12) ---
|
||||
|
||||
@Test
|
||||
fun testPaySuccessWithPayerProofDeserialization() {
|
||||
val json =
|
||||
"""{"result_type":"pay","result":{"state":"settled","instruction_type":"bolt12","amount":21000,"fees_paid":100,"payment_hash":"abc","preimage":"def","payer_proof":"lnp1xyz","settled_at":1693876497}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<PaySuccessResponse>(response)
|
||||
assertEquals("settled", response.result?.state)
|
||||
assertEquals("bolt12", response.result?.instruction_type)
|
||||
assertEquals(21_000L, response.result?.amount)
|
||||
assertEquals(100L, response.result?.fees_paid)
|
||||
assertEquals("abc", response.result?.payment_hash)
|
||||
assertEquals("def", response.result?.preimage)
|
||||
assertEquals("lnp1xyz", response.result?.payer_proof)
|
||||
assertEquals(1693876497L, response.result?.settled_at)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPaySuccessWithoutPayerProof() {
|
||||
// payer_proof is best-effort ("optional if unavailable") — absence must parse fine.
|
||||
val json = """{"result_type":"pay","result":{"state":"settled","preimage":"def"}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<PaySuccessResponse>(response)
|
||||
assertEquals("def", response.result?.preimage)
|
||||
assertNull(response.result?.payer_proof)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPayErrorUnsupportedInstruction() {
|
||||
val json = """{"result_type":"pay","error":{"code":"UNSUPPORTED_PAYMENT_INSTRUCTION","message":"no bolt12"}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<NwcErrorResponse>(response)
|
||||
assertEquals(NwcErrorCode.UNSUPPORTED_PAYMENT_INSTRUCTION, response.error?.code)
|
||||
}
|
||||
|
||||
// --- Receive Success (nwc#2) ---
|
||||
|
||||
@Test
|
||||
fun testReceiveSuccessDeserialization() {
|
||||
val json = """{"result_type":"receive","result":{"bip321":"bitcoin:?lightning=lnbc1&lno=lno1","transaction_id":"tx1"}}"""
|
||||
val response = OptimizedJsonMapper.fromJsonTo<Response>(json)
|
||||
assertIs<ReceiveSuccessResponse>(response)
|
||||
assertEquals("bitcoin:?lightning=lnbc1&lno=lno1", response.result?.bip321)
|
||||
assertEquals("tx1", response.result?.transaction_id)
|
||||
}
|
||||
|
||||
// --- PayKeysend Success ---
|
||||
|
||||
@Test
|
||||
|
||||
+4
@@ -36,6 +36,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
@@ -51,6 +53,8 @@ class RequestDeserializer : StdDeserializer<Request>(Request::class.java) {
|
||||
|
||||
return when (method) {
|
||||
NwcMethod.PAY_INVOICE -> jp.codec.treeToValue(jsonObject, PayInvoiceMethod::class.java)
|
||||
NwcMethod.PAY -> jp.codec.treeToValue(jsonObject, PayMethod::class.java)
|
||||
NwcMethod.RECEIVE -> jp.codec.treeToValue(jsonObject, ReceiveMethod::class.java)
|
||||
NwcMethod.PAY_KEYSEND -> jp.codec.treeToValue(jsonObject, PayKeysendMethod::class.java)
|
||||
NwcMethod.MAKE_INVOICE -> jp.codec.treeToValue(jsonObject, MakeInvoiceMethod::class.java)
|
||||
NwcMethod.LOOKUP_INVOICE -> jp.codec.treeToValue(jsonObject, LookupInvoiceMethod::class.java)
|
||||
|
||||
+14
@@ -31,6 +31,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
@@ -52,6 +54,18 @@ class RequestSerializer : StdSerializer<Request>(Request::class.java) {
|
||||
}
|
||||
}
|
||||
|
||||
is PayMethod -> {
|
||||
if (value.params != null) {
|
||||
gen.writeObjectField("params", value.params)
|
||||
}
|
||||
}
|
||||
|
||||
is ReceiveMethod -> {
|
||||
if (value.params != null) {
|
||||
gen.writeObjectField("params", value.params)
|
||||
}
|
||||
}
|
||||
|
||||
is PayKeysendMethod -> {
|
||||
if (value.params != null) {
|
||||
gen.writeObjectField("params", value.params)
|
||||
|
||||
+10
@@ -39,6 +39,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
@@ -73,6 +75,14 @@ class ResponseDeserializer : StdDeserializer<Response>(Response::class.java) {
|
||||
jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java)
|
||||
}
|
||||
|
||||
NwcMethod.PAY -> {
|
||||
jp.codec.treeToValue(jsonObject, PaySuccessResponse::class.java)
|
||||
}
|
||||
|
||||
NwcMethod.RECEIVE -> {
|
||||
jp.codec.treeToValue(jsonObject, ReceiveSuccessResponse::class.java)
|
||||
}
|
||||
|
||||
NwcMethod.PAY_KEYSEND -> {
|
||||
jp.codec.treeToValue(jsonObject, PayKeysendSuccessResponse::class.java)
|
||||
}
|
||||
|
||||
+14
@@ -36,6 +36,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ReceiveSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
@@ -69,6 +71,18 @@ class ResponseSerializer : StdSerializer<Response>(Response::class.java) {
|
||||
}
|
||||
}
|
||||
|
||||
is PaySuccessResponse -> {
|
||||
if (value.result != null) {
|
||||
gen.writeObjectField("result", value.result)
|
||||
}
|
||||
}
|
||||
|
||||
is ReceiveSuccessResponse -> {
|
||||
if (value.result != null) {
|
||||
gen.writeObjectField("result", value.result)
|
||||
}
|
||||
}
|
||||
|
||||
is PayKeysendSuccessResponse -> {
|
||||
if (value.result != null) {
|
||||
gen.writeObjectField("result", value.result)
|
||||
|
||||
Reference in New Issue
Block a user