Files
damus/damus/Features/Wallet/Models/WalletConnect/Response.swift
T
Copilot 7be75f37c6 Fix: Gracefully ignore unsupported NWC response types (e.g. get_info)
When another NWC client (e.g. Alby) connected to the same relay calls
`get_info`, Damus receives the response and previously threw a
DecodingError.typeMismatch, causing an "Oops" error dialog to be shown.

Fix: Make `result_type` optional in `WalletConnect.Response`. Unknown
result types now decode without throwing — `result_type` and `result`
are set to `nil`, and the rest of the existing nil-guarded code paths
handle this silently.

Adds a test to verify `get_info` (and any future unknown result type)
is decoded gracefully.

Closes: #2204
Changelog-Fixed: Fixed issue where the app could display an error message when using another NWC wallet in parallel
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: danieldaquino <24692108+danieldaquino@users.noreply.github.com>
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2026-02-23 12:50:25 -08:00

162 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Response.swift
// damus
//
// Created by Daniel DAquino on 2025-03-10.
//
import Combine
extension WalletConnect {
/// Models a response from the NWC provider
struct Response: Decodable {
/// The type of the result. `nil` if the result type is unsupported/unknown.
let result_type: Response.Result.ResultType?
let error: WalletResponseErr?
let result: Response.Result?
private enum CodingKeys: CodingKey {
case result_type, error, result
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let result_type_str = try container.decode(String.self, forKey: .result_type)
let result_type = Response.Result.ResultType(rawValue: result_type_str)
self.result_type = result_type
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
guard let result_type else {
// Unknown/unsupported result type gracefully ignore without an error
self.result = nil
return
}
guard self.error == nil else {
self.result = nil
return
}
switch result_type {
case .pay_invoice:
let res = try container.decode(Result.PayInvoiceResponse.self, forKey: .result)
self.result = .pay_invoice(res)
case .get_balance:
let res = try container.decode(Result.GetBalanceResponse.self, forKey: .result)
self.result = .get_balance(res)
case .list_transactions:
let res = try container.decode(Result.ListTransactionsResponse.self, forKey: .result)
self.result = .list_transactions(res)
}
}
}
struct FullWalletResponse {
let req_id: NoteId
let response: Response
init(from event: NostrEvent, nwc: WalletConnect.ConnectURL) throws(InitializationError) {
guard event.pubkey == nwc.pubkey else { throw .incorrectAuthorPubkey }
guard let referencedNoteId = event.referenced_ids.first else { throw .missingRequestIdReference }
self.req_id = referencedNoteId
var json = ""
do {
json = try NIP04.decryptContent(
recipientPrivateKey: nwc.keypair.privkey,
senderPubkey: nwc.pubkey,
content: event.content,
encoding: .base64
)
}
catch { throw .failedToDecrypt(error) }
do {
let response: WalletConnect.Response = try decode_json_throwing(json)
self.response = response
}
catch { throw .failedToDecodeJSON(error) }
}
enum InitializationError: Error {
case incorrectAuthorPubkey
case missingRequestIdReference
case failedToDecodeJSON(any Error)
case failedToDecrypt(any Error)
}
}
struct WalletResponseErr: Codable, Error {
let code: Code?
let message: String?
enum Code: String, Codable {
/// The client is sending commands too fast. It should retry in a few seconds.
case rateLimited = "RATE_LIMITED"
/// The command is not known or is intentionally not implemented.
case notImplemented = "NOT_IMPLEMENTED"
/// The wallet does not have enough funds to cover a fee reserve or the payment amount.
case insufficientBalance = "INSUFFICIENT_BALANCE"
/// The wallet has exceeded its spending quota.
case quotaExceeded = "QUOTA_EXCEEDED"
/// This public key is not allowed to do this operation.
case restricted = "RESTRICTED"
/// This public key has no wallet connected.
case unauthorized = "UNAUTHORIZED"
/// An internal error.
case internalError = "INTERNAL"
/// Other error.
case other = "OTHER"
}
enum CodingKeys: String, CodingKey {
case code, message
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Attempt to decode the code as a String
if let codeString = try container.decodeIfPresent(String.self, forKey: .code),
let validCode = Code(rawValue: codeString) {
self.code = validCode
} else {
// If the code is either missing or not one of the allowed cases, set it to nil
self.code = nil
}
self.message = try container.decodeIfPresent(String.self, forKey: .message)
}
}
}
extension WalletConnect.Response {
/// The response data resulting from an NWC request
enum Result {
case pay_invoice(PayInvoiceResponse)
case get_balance(GetBalanceResponse)
case list_transactions(ListTransactionsResponse)
enum ResultType: String {
case pay_invoice
case get_balance
case list_transactions
}
struct PayInvoiceResponse: Decodable {
let preimage: String
}
struct GetBalanceResponse: Decodable {
let balance: Int64
}
struct ListTransactionsResponse: Decodable {
let transactions: [WalletConnect.Transaction]
}
}
}