@@ -20,8 +20,44 @@ class AddressValidator {
2020 _ currency: String ?
2121 ) -> Void
2222
23-
23+ enum ResolutionError : Error {
24+ case missingApiToken
25+ case httpStatus( Int )
26+ case emptyResponse
27+ case noRecords
28+ case recordNotFound
29+ case invalidJson
30+ case invalidResolvedAddress
31+ }
32+
33+ private enum UnstoppableDomains {
34+ static let apiBaseUrl = " https://api.unstoppabledomains.com/resolve/domains/ "
35+ static let recordKey = " crypto.XVG.address "
36+
37+ static var apiToken : String ? {
38+ return Bundle . main. object ( forInfoDictionaryKey: " UNSTOPPABLE_DOMAINS_API_TOKEN " ) as? String
39+ }
40+
41+ static func looksLikeDomain( _ value: String ) -> Bool {
42+ let trimmed = value. trimmingCharacters ( in: . whitespacesAndNewlines) . lowercased ( )
43+
44+ return trimmed. firstIndex ( of: " . " ) != nil
45+ && trimmed. firstIndex ( of: " . " ) != trimmed. startIndex
46+ && !trimmed. contains ( " " )
47+ && !trimmed. contains ( " :// " )
48+ && !trimmed. contains ( " / " )
49+ }
50+ }
51+
52+ private struct UnstoppableDomainsResponse : Decodable {
53+ let records : [ String : String ] ?
54+ }
55+
2456 static func validate( address: String ) -> Bool {
57+ if validateVergeLegacy ( address: address) {
58+ return true
59+ }
60+
2561 // Try legacy first
2662 if let _ = try ? BitcoinAddress ( legacy: address) {
2763 return true
@@ -35,6 +71,17 @@ class AddressValidator {
3571 return false
3672 }
3773
74+ private static func validateVergeLegacy( address: String ) -> Bool {
75+ guard let payload = Base58Check . decode ( address) , payload. count == 21 else {
76+ return false
77+ }
78+
79+ let versionByte = payload [ 0 ]
80+
81+ return versionByte == Network . mainnetXVG. pubkeyhash
82+ || versionByte == Network . mainnetXVG. scripthash
83+ }
84+
3885
3986
4087 func validate(
@@ -45,21 +92,56 @@ class AddressValidator {
4592 }
4693
4794 func validate( string: String , completion: @escaping ValidationCompletion ) {
95+ validate ( string: string, resolveDomains: false ) { valid, address, amount, label, currency, _ in
96+ completion ( valid, address, amount, label, currency)
97+ }
98+ }
99+
100+ func validateOrResolve( string: String , completion: @escaping ValidationCompletion ) {
101+ validateOrResolve ( string: string) { valid, address, amount, label, currency, _ in
102+ completion ( valid, address, amount, label, currency)
103+ }
104+ }
105+
106+ func validateOrResolve(
107+ string: String ,
108+ completion: @escaping (
109+ _ valid: Bool ,
110+ _ address: String ? ,
111+ _ amount: NSNumber ? ,
112+ _ label: String ? ,
113+ _ currency: String ? ,
114+ _ error: ResolutionError ?
115+ ) -> Void
116+ ) {
117+ validate ( string: string, resolveDomains: true , completion: completion)
118+ }
119+
120+ private func validate(
121+ string: String ,
122+ resolveDomains: Bool ,
123+ completion: @escaping (
124+ _ valid: Bool ,
125+ _ address: String ? ,
126+ _ amount: NSNumber ? ,
127+ _ label: String ? ,
128+ _ currency: String ? ,
129+ _ error: ResolutionError ?
130+ ) -> Void
131+ ) {
48132 var valid = false
49133 var address : String ?
50134 var amount : NSNumber ?
51135 var label : String ?
52136 var currency : String ?
53137
54138 let parameters = self . normalizeUrl ( url: string)
139+ let addressParam = parameters [ " address " ] ?? nil
55140
56- guard let addressParam = parameters [ " address " ] , AddressValidator . validate ( address : addressParam ?? " " ) else {
57- return completion ( valid, address, amount, label, currency)
141+ guard let recipient = addressParam else {
142+ return completion ( valid, address, amount, label, currency, nil )
58143 }
59144
60- address = addressParam
61- valid = true
62-
63145 if let amountParam = parameters [ " amount " ] , amountParam != nil {
64146 amount = self . amountToNumber ( stringAmount: amountParam!)
65147 }
@@ -72,7 +154,29 @@ class AddressValidator {
72154 currency = currencyParam? . uppercased ( ) == " XVG " ? nil : currencyParam
73155 }
74156
75- completion ( valid, address, amount, label, currency)
157+ if AddressValidator . validate ( address: recipient) {
158+ valid = true
159+ address = recipient
160+
161+ return completion ( valid, address, amount, label, currency, nil )
162+ }
163+
164+ guard resolveDomains, UnstoppableDomains . looksLikeDomain ( recipient) else {
165+ return completion ( valid, address, amount, label, currency, nil )
166+ }
167+
168+ resolveUnstoppableDomain ( recipient) { result in
169+ switch result {
170+ case . success( let resolvedAddress) :
171+ guard AddressValidator . validate ( address: resolvedAddress) else {
172+ return completion ( false , nil , amount, label, currency, . invalidResolvedAddress)
173+ }
174+
175+ completion ( true , resolvedAddress, amount, label, currency, nil )
176+ case . failure( let error) :
177+ completion ( false , nil , amount, label, currency, error)
178+ }
179+ }
76180 }
77181
78182 fileprivate func amountToNumber( stringAmount: String ) -> NSNumber ? {
@@ -104,4 +208,55 @@ class AddressValidator {
104208
105209 return parameters
106210 }
211+
212+ private func resolveUnstoppableDomain( _ domain: String , completion: @escaping ( Result < String , ResolutionError > ) -> Void ) {
213+ guard let token = UnstoppableDomains . apiToken, !token. isEmpty else {
214+ return completion ( . failure( . missingApiToken) )
215+ }
216+
217+ let trimmedDomain = domain. trimmingCharacters ( in: . whitespacesAndNewlines) . lowercased ( )
218+ guard let encodedDomain = trimmedDomain. addingPercentEncoding ( withAllowedCharacters: . urlPathAllowed) ,
219+ let url = URL ( string: UnstoppableDomains . apiBaseUrl + encodedDomain)
220+ else {
221+ return completion ( . failure( . recordNotFound) )
222+ }
223+
224+ var request = URLRequest ( url: url)
225+ request. setValue ( " Bearer \( token) " , forHTTPHeaderField: " Authorization " )
226+
227+ URLSession . shared. dataTask ( with: request) { data, response, _ in
228+ guard let httpResponse = response as? HTTPURLResponse else {
229+ return completion ( . failure( . emptyResponse) )
230+ }
231+
232+ guard httpResponse. statusCode == 200 else {
233+ return completion ( . failure( . httpStatus( httpResponse. statusCode) ) )
234+ }
235+
236+ guard let data = data, !data. isEmpty else {
237+ return completion ( . failure( . emptyResponse) )
238+ }
239+
240+ guard let decoded = try ? JSONDecoder ( ) . decode ( UnstoppableDomainsResponse . self, from: data) else {
241+ return completion ( . failure( . invalidJson) )
242+ }
243+
244+ guard let records = decoded. records else {
245+ return completion ( . failure( . noRecords) )
246+ }
247+
248+ guard let address = records [ UnstoppableDomains . recordKey] , !address. isEmpty else {
249+ return completion ( . failure( . recordNotFound) )
250+ }
251+
252+ completion ( . success( self . normalizedResolvedAddress ( address) ) )
253+ } . resume ( )
254+ }
255+
256+ private func normalizedResolvedAddress( _ address: String ) -> String {
257+ return address
258+ . trimmingCharacters ( in: . whitespacesAndNewlines)
259+ . replacingOccurrences ( of: " verge:// " , with: " " )
260+ . replacingOccurrences ( of: " verge: " , with: " " )
261+ }
107262}
0 commit comments