WalletConnect.swift (4851B)
1 // 2 // WalletConnect.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-03-22. 6 // 7 8 import Foundation 9 10 struct WalletConnectURL: Equatable { 11 static func == (lhs: WalletConnectURL, rhs: WalletConnectURL) -> Bool { 12 return lhs.keypair == rhs.keypair && 13 lhs.pubkey == rhs.pubkey && 14 lhs.relay == rhs.relay 15 } 16 17 let relay: RelayURL 18 let keypair: FullKeypair 19 let pubkey: Pubkey 20 let lud16: String? 21 22 func to_url() -> URL { 23 var urlComponents = URLComponents() 24 urlComponents.scheme = "nostrwalletconnect" 25 urlComponents.host = pubkey.hex() 26 urlComponents.queryItems = [ 27 URLQueryItem(name: "relay", value: relay.absoluteString), 28 URLQueryItem(name: "secret", value: keypair.privkey.hex()) 29 ] 30 31 if let lud16 { 32 urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16)) 33 } 34 35 return urlComponents.url! 36 } 37 38 init?(str: String) { 39 guard let components = URLComponents(string: str), 40 components.scheme == "nostrwalletconnect" || components.scheme == "nostr+walletconnect", 41 // The line below provides flexibility for both `nostrwalletconnect://` (non-compliant, but commonly used) and `nostrwalletconnect:` (NIP-47 compliant) formats 42 let encoded_pubkey = components.path == "" ? components.host : components.path, 43 let pubkey = hex_decode_pubkey(encoded_pubkey), 44 let items = components.queryItems, 45 let relay = items.first(where: { qi in qi.name == "relay" })?.value, 46 let relay_url = RelayURL(relay), 47 let secret = items.first(where: { qi in qi.name == "secret" })?.value, 48 secret.utf8.count == 64, 49 let decoded = hex_decode(secret) 50 else { 51 return nil 52 } 53 54 let privkey = Privkey(Data(decoded)) 55 guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil } 56 57 let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value 58 let keypair = FullKeypair(pubkey: our_pk, privkey: privkey) 59 self = WalletConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16) 60 } 61 62 init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) { 63 self.pubkey = pubkey 64 self.relay = relay 65 self.keypair = keypair 66 self.lud16 = lud16 67 } 68 } 69 70 struct WalletRequest<T: Codable>: Codable { 71 let method: String 72 let params: T? 73 } 74 75 struct WalletResponseErr: Codable { 76 let code: String? 77 let message: String? 78 } 79 80 struct PayInvoiceResponse: Decodable { 81 let preimage: String 82 } 83 84 enum WalletResponseResultType: String { 85 case pay_invoice 86 } 87 88 enum WalletResponseResult { 89 case pay_invoice(PayInvoiceResponse) 90 } 91 92 struct FullWalletResponse { 93 let req_id: NoteId 94 let response: WalletResponse 95 96 init?(from: NostrEvent, nwc: WalletConnectURL) async { 97 guard let note_id = from.referenced_ids.first else { 98 return nil 99 } 100 101 self.req_id = note_id 102 103 let ares = Task { 104 guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64), 105 let resp: WalletResponse = decode_json(json) 106 else { 107 let resp: WalletResponse? = nil 108 return resp 109 } 110 111 return resp 112 } 113 114 guard let res = await ares.value else { 115 return nil 116 } 117 118 self.response = res 119 } 120 121 } 122 123 struct WalletResponse: Decodable { 124 let result_type: WalletResponseResultType 125 let error: WalletResponseErr? 126 let result: WalletResponseResult? 127 128 private enum CodingKeys: CodingKey { 129 case result_type, error, result 130 } 131 132 init(from decoder: Decoder) throws { 133 let container = try decoder.container(keyedBy: CodingKeys.self) 134 let result_type_str = try container.decode(String.self, forKey: .result_type) 135 136 guard let result_type = WalletResponseResultType(rawValue: result_type_str) else { 137 throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown")) 138 } 139 140 self.result_type = result_type 141 self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error) 142 143 guard self.error == nil else { 144 self.result = nil 145 return 146 } 147 148 switch result_type { 149 case .pay_invoice: 150 let res = try container.decode(PayInvoiceResponse.self, forKey: .result) 151 self.result = .pay_invoice(res) 152 } 153 } 154 } 155