Files
damus/damus/Util/WalletConnect/Request.swift
T
Daniel D’Aquino 309b00380d Add description and metadata to pay_invoice command
Changelog-Added: Zap receiver information now included for outgoing zaps
Closes: https://github.com/damus-io/damus/issues/2927
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-05-15 15:12:52 -07:00

178 lines
8.2 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.
//
// Request.swift
// damus
//
// Created by Daniel DAquino on 2025-03-10.
//
import Foundation
extension WalletConnect {
/// Models a request to an NWC wallet provider
enum Request: Codable {
/// Pay an invoice
case payInvoice(
/// bolt-11 invoice string
invoice: String,
/// The full description of the invoice (If description does not fit in the BOLT-11 invoice, this is the pre-image of the description hash)
description: String?,
/// Optional metadata object containing more information
metadata: Metadata?
)
/// Get the current wallet balance
case getBalance
/// Get the current wallet transaction history
case getTransactionList(
/// Starting timestamp in seconds since epoch (inclusive), optional.
from: UInt64?,
/// Ending timestamp in seconds since epoch (inclusive), optional.
until: UInt64?,
/// Maximum number of invoices to return, optional.
limit: Int?,
/// Offset of the first invoice to return, optional.
offset: Int?,
/// Include unpaid invoices, optional, default false.
unpaid: Bool?,
/// "incoming" for invoices, "outgoing" for payments, undefined for both.
type: String?
)
static func payZapRequest(invoice: String, zapRequest: NostrEvent?) -> Self {
guard let zapRequest, let zapRequestEncoded = encode_json(zapRequest) else {
return WalletConnect.Request.payInvoice(
invoice: invoice,
description: nil,
metadata: nil
)
}
return WalletConnect.Request.payInvoice(
invoice: invoice,
description: zapRequestEncoded,
metadata: .init(nostr: zapRequest)
)
}
struct Metadata: Codable, Equatable, Hashable {
/// NIP-57-compliant `kind:9734` zap request event
let nostr: NostrEvent?
init(nostr: NostrEvent?) {
self.nostr = nostr
}
init(from decoder: any Decoder) throws {
let container: KeyedDecodingContainer<WalletConnect.Request.Metadata.CodingKeys> = try decoder.container(keyedBy: WalletConnect.Request.Metadata.CodingKeys.self)
guard let decodedZapRequest = try? container.decodeIfPresent(NostrEvent.self, forKey: WalletConnect.Request.Metadata.CodingKeys.nostr) else {
self.nostr = nil // Be lenient and fallback to nil if the NWC provider provided something invalid, since metadata is not strictly spec'd yet.
return
}
self.nostr = decodedZapRequest
}
}
// MARK: - Interface
/// Converts the NWC request into a raw Nostr event to be sent in the network
///
/// - Parameters:
/// - to_pk: The destination pubkey (used for encryption)
/// - keypair: The requester's pubkey (used for encryption and signing)
/// - Returns: The NWC request in a raw Nostr Event format, or nil if it cannot be encoded
func to_nostr_event(to_pk: Pubkey, keypair: FullKeypair) -> NostrEvent? {
let tags = [to_pk.tag]
let created_at = UInt32(Date().timeIntervalSince1970)
guard let content = encode_json(self) else {
return nil
}
return NIP04.create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: NostrKind.nwc_request.rawValue)
}
// MARK: - Encoding and decoding
/// Keys for top-level JSON
private enum CodingKeys: String, CodingKey {
case method
case params
}
/// Keys for the JSON inside the "params" object
private enum ParamKeys: String, CodingKey {
case invoice, description, metadata
case from, until, limit, offset, unpaid, type
}
/// Constants for possible request "method" verbs
private enum Method: String {
case payInvoice = "pay_invoice"
case getBalance = "get_balance"
case listTransactions = "list_transactions"
}
/// Decodes a payload into this request structure
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let method = try container.decode(String.self, forKey: .method)
switch method {
case Method.payInvoice.rawValue:
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
let invoice = try paramsContainer.decode(String.self, forKey: .invoice)
let description: String? = try paramsContainer.decodeIfPresent(String.self, forKey: .description)
let metadata: Metadata? = try paramsContainer.decodeIfPresent(Metadata.self, forKey: .metadata)
self = .payInvoice(invoice: invoice, description: description, metadata: metadata)
case Method.getBalance.rawValue:
// No params to decode
self = .getBalance
case Method.listTransactions.rawValue:
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
let from = try paramsContainer.decodeIfPresent(UInt64.self, forKey: .from)
let until = try paramsContainer.decodeIfPresent(UInt64.self, forKey: .until)
let limit = try paramsContainer.decodeIfPresent(Int.self, forKey: .limit)
let offset = try paramsContainer.decodeIfPresent(Int.self, forKey: .offset)
let unpaid = try paramsContainer.decodeIfPresent(Bool.self, forKey: .unpaid)
let type = try paramsContainer.decodeIfPresent(String.self, forKey: .type)
self = .getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type)
default:
throw DecodingError.dataCorruptedError(
forKey: .method,
in: container,
debugDescription: "Unknown wallet method \"\(method)\""
)
}
}
/// Encodes this request structure into a payload
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .payInvoice(let invoice, let description, let metadata):
try container.encode(Method.payInvoice.rawValue, forKey: .method)
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
try paramsContainer.encode(invoice, forKey: .invoice)
try paramsContainer.encodeIfPresent(description, forKey: .description)
try paramsContainer.encodeIfPresent(metadata, forKey: .metadata)
case .getBalance:
try container.encode(Method.getBalance.rawValue, forKey: .method)
// "params": null
try container.encodeNil(forKey: .params)
case .getTransactionList(let from, let until, let limit, let offset, let unpaid, let type):
try container.encode(Method.listTransactions.rawValue, forKey: .method)
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
try paramsContainer.encodeIfPresent(from, forKey: .from)
try paramsContainer.encodeIfPresent(until, forKey: .until)
try paramsContainer.encodeIfPresent(limit, forKey: .limit)
try paramsContainer.encodeIfPresent(offset, forKey: .offset)
try paramsContainer.encodeIfPresent(unpaid, forKey: .unpaid)
try paramsContainer.encodeIfPresent(type, forKey: .type)
}
}
}
}