From 17a0811d1304526cd7e564fcfd28be1792953262 Mon Sep 17 00:00:00 2001 From: Sergey Merenkov Date: Thu, 10 Sep 2020 19:01:14 +0300 Subject: [PATCH] [#174576563] Add ZNS support to swift resolution library --- Resolution.xcodeproj/project.pbxproj | 22 ++- Sources/Resolution/ABI/JSON_RPC.swift | 29 +++- Sources/Resolution/Contract.swift | 7 +- Sources/Resolution/ContractZNS.swift | 97 +++++++++++++ Sources/Resolution/Errors.swift | 1 + Sources/Resolution/Helpers/Types.swift | 12 ++ .../Resolution/{ => Helpers}/Utilities.swift | 0 .../NamingServices/CommonNamingService.swift | 9 +- Sources/Resolution/NamingServices/ZNS.swift | 136 ++++++++++++++++++ Sources/Resolution/Resolution.swift | 6 +- Tests/ResolutionTests/ResolutionTests.swift | 28 +++- 11 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 Sources/Resolution/ContractZNS.swift create mode 100644 Sources/Resolution/Helpers/Types.swift rename Sources/Resolution/{ => Helpers}/Utilities.swift (100%) create mode 100644 Sources/Resolution/NamingServices/ZNS.swift diff --git a/Resolution.xcodeproj/project.pbxproj b/Resolution.xcodeproj/project.pbxproj index 689d522..609fb54 100644 --- a/Resolution.xcodeproj/project.pbxproj +++ b/Resolution.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ 3EE4DA1424E367720097540B /* resolution.h in Headers */ = {isa = PBXBuildFile; fileRef = 3EE4DA0624E367720097540B /* resolution.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3EE4DA1E24E367D40097540B /* Resolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EE4DA1D24E367D40097540B /* Resolution.swift */; }; 3EF4D83524F77E300022B33C /* AbiCoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF4D83424F77E300022B33C /* AbiCoderTests.swift */; }; + 8402F708250935FE0007F01D /* ContractZNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8402F707250935FD0007F01D /* ContractZNS.swift */; }; + 848E21092507890A008707D2 /* ZNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848E21082507890A008707D2 /* ZNS.swift */; }; + 848E210C25079D61008707D2 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848E210B25079D61008707D2 /* Types.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -58,7 +61,10 @@ 3EE4DA1324E367720097540B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3EE4DA1D24E367D40097540B /* Resolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolution.swift; sourceTree = ""; }; 3EF4D83424F77E300022B33C /* AbiCoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbiCoderTests.swift; sourceTree = ""; }; + 8402F707250935FD0007F01D /* ContractZNS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContractZNS.swift; sourceTree = ""; }; 8425AED824FFBADF00BBCBBA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 848E21082507890A008707D2 /* ZNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZNS.swift; sourceTree = ""; }; + 848E210B25079D61008707D2 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +91,7 @@ isa = PBXGroup; children = ( 3E2AC91B24E4FA28008EBC39 /* CNS.swift */, + 848E21082507890A008707D2 /* ZNS.swift */, 3EAFA40324EE32B2008791E9 /* CommonNamingService.swift */, ); path = NamingServices; @@ -140,6 +147,7 @@ 3EE4DA0524E367720097540B /* Resolution */ = { isa = PBXGroup; children = ( + 848E210A25079D43008707D2 /* Helpers */, 3E2AC92024E4FA3E008EBC39 /* ABI */, 3E2AC91D24E4FA35008EBC39 /* Protocols */, 3E2AC91A24E4FA28008EBC39 /* NamingServices */, @@ -147,8 +155,8 @@ 3EE4DA1D24E367D40097540B /* Resolution.swift */, 3E65912724E381E900D7EC35 /* Errors.swift */, 3E65913024E4ADAC00D7EC35 /* Contract.swift */, + 8402F707250935FD0007F01D /* ContractZNS.swift */, 3EAFA3FF24EE0CFD008791E9 /* APIRequest.swift */, - 3EAFA40124EE312A008791E9 /* Utilities.swift */, ); name = Resolution; path = Sources/Resolution; @@ -173,6 +181,15 @@ path = Sources; sourceTree = ""; }; + 848E210A25079D43008707D2 /* Helpers */ = { + isa = PBXGroup; + children = ( + 3EAFA40124EE312A008791E9 /* Utilities.swift */, + 848E210B25079D61008707D2 /* Types.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 849E07CB24F946CA00A793D3 /* Tests */ = { isa = PBXGroup; children = ( @@ -324,14 +341,17 @@ 3E2AC91C24E4FA28008EBC39 /* CNS.swift in Sources */, 3EAFA40424EE32B2008791E9 /* CommonNamingService.swift in Sources */, 3E2AC91F24E4FA35008EBC39 /* NamingService.swift in Sources */, + 848E21092507890A008707D2 /* ZNS.swift in Sources */, 3E2AC94124E5EE9A008EBC39 /* ABIElement.swift in Sources */, 3E65912824E381E900D7EC35 /* Errors.swift in Sources */, 3EAFA3F724EB59DA008791E9 /* ABICoder.swift in Sources */, 3EE4DA1E24E367D40097540B /* Resolution.swift in Sources */, + 848E210C25079D61008707D2 /* Types.swift in Sources */, 3E65913124E4ADAC00D7EC35 /* Contract.swift in Sources */, 3EAFA40024EE0CFD008791E9 /* APIRequest.swift in Sources */, 3EAFA40224EE312A008791E9 /* Utilities.swift in Sources */, 3EAFA3FE24EE03E0008791E9 /* JSON_RPC.swift in Sources */, + 8402F708250935FE0007F01D /* ContractZNS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Resolution/ABI/JSON_RPC.swift b/Sources/Resolution/ABI/JSON_RPC.swift index 3b9f7ae..b82cb38 100644 --- a/Sources/Resolution/ABI/JSON_RPC.swift +++ b/Sources/Resolution/ABI/JSON_RPC.swift @@ -12,11 +12,20 @@ import Foundation struct JSON_RPC_REQUEST: Codable { let jsonrpc, id, method: String let params: [ParamElement] + + enum CodingKeys: String, CodingKey { + case jsonrpc + case id + case method + case params + } } enum ParamElement: Codable { case paramClass(ParamClass) case string(String) + case array([ParamElement]) + case dictionary([String: ParamElement]) init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -28,6 +37,15 @@ enum ParamElement: Codable { self = .paramClass(elem) return } + if let elem = try? container.decode(Array.self) { + self = .array(elem) + return + } + if let elem = try? container.decode([String: ParamElement].self) { + self = .dictionary(elem) + return + } + throw DecodingError.typeMismatch(ParamElement.self, DecodingError.Context( codingPath: decoder.codingPath, @@ -42,14 +60,21 @@ enum ParamElement: Codable { try container.encode(elem) case .string(let elem): try container.encode(elem) + case .array(let array): + try container.encode(array) + case .dictionary(let dict): + try container.encode(dict) } } } struct ParamClass: Codable { - let data, to: String + let data: String + let to: String } struct JSON_RPC_RESPONSE: Codable { - let jsonrpc, id, result: String + let jsonrpc: String + let id: String + let result: ParamElement } diff --git a/Sources/Resolution/Contract.swift b/Sources/Resolution/Contract.swift index b39664e..73b9d03 100644 --- a/Sources/Resolution/Contract.swift +++ b/Sources/Resolution/Contract.swift @@ -51,6 +51,11 @@ internal class Contract { guard err == nil else { throw err! } - return resp?.result + switch resp?.result { + case .string(let result): + return result + default: + return nil + } } } diff --git a/Sources/Resolution/ContractZNS.swift b/Sources/Resolution/ContractZNS.swift new file mode 100644 index 0000000..d381aa8 --- /dev/null +++ b/Sources/Resolution/ContractZNS.swift @@ -0,0 +1,97 @@ +// +// Contract.swift +// resolution +// +// Created by Johnny Good on 8/12/20. +// Copyright © 2020 Johnny Good. All rights reserved. +// + +import Foundation + +internal class ContractZNS { + let address: String + let providerUrl: String + + init(providerUrl: String, address: String) { + self.address = address + self.providerUrl = providerUrl + } + + func fetchSubState(field: String, keys: [String]) throws -> Any { + + let body: JSON_RPC_REQUEST = JSON_RPC_REQUEST( + jsonrpc: "2.0", + id: "1", + method: "GetSmartContractSubState", + params: [ + ParamElement.string(self.address), + ParamElement.string(field), + ParamElement.array(keys.map { ParamElement.string($0) }) + ] + ) + let response = try postRequest(body)! + + guard case let ParamElement.dictionary(dict) = response, + let results = self.reduce(dict: dict)[field] as? [String: Any] else { + print("Invalid response, can't process") + return response + } + + return results + } + + private func postRequest(_ body: JSON_RPC_REQUEST) throws -> Any? { + let postRequest = APIRequest(providerUrl) + var resp: JSON_RPC_RESPONSE? + var err: Error? + let semaphore = DispatchSemaphore(value: 0) + postRequest.post(body, completion: {result in + switch result { + case .success(let response): + resp = response + case .failure(let error): + err = error + } + semaphore.signal() + }) + semaphore.wait() + guard err == nil else { + throw err! + } + return resp?.result + } + + // MARK: - PRIVATE Helper functions + + private func reduce(dict: [String: ParamElement]) -> [String: Any] { + return dict.reduce(into: [String: Any]()) { dict, pair in + let (key, value) = pair + + switch value { + case .paramClass(let elem): + dict[key] = elem + case .string(let elem): + dict[key] = elem + case .array(let array): + dict[key] = self.map(array: array) + case .dictionary(let dictionary): + dict[key] = self.reduce(dict: dictionary) + } + } + } + + private func map(array: [ParamElement]) -> [Any] { + return array.map { (value) -> Any in + switch value { + case .paramClass(let elem): + return elem + case .string(let elem): + return elem + case .array(let array): + return self.map(array: array) + case .dictionary(let dictionary): + return self.reduce(dict: dictionary) + } + } + } +} diff --git a/Sources/Resolution/Errors.swift b/Sources/Resolution/Errors.swift index d777c83..84847e3 100644 --- a/Sources/Resolution/Errors.swift +++ b/Sources/Resolution/Errors.swift @@ -14,5 +14,6 @@ public enum ResolutionError: Error { case unconfiguredDomain case recordNotFound case unsupportedNetwork + case unspecifiedResolver case unknownError(Error) } diff --git a/Sources/Resolution/Helpers/Types.swift b/Sources/Resolution/Helpers/Types.swift new file mode 100644 index 0000000..306d659 --- /dev/null +++ b/Sources/Resolution/Helpers/Types.swift @@ -0,0 +1,12 @@ +// +// Types.swift +// Resolution +// +// Created by Admin on 08.09.20. +// Copyright © 2020 Johnny Good. All rights reserved. +// + +import Foundation + +public typealias StringResult = (Result) -> Void +public typealias DictionaryResult = (Result<[String: String], ResolutionError>) -> Void diff --git a/Sources/Resolution/Utilities.swift b/Sources/Resolution/Helpers/Utilities.swift similarity index 100% rename from Sources/Resolution/Utilities.swift rename to Sources/Resolution/Helpers/Utilities.swift diff --git a/Sources/Resolution/NamingServices/CommonNamingService.swift b/Sources/Resolution/NamingServices/CommonNamingService.swift index c29b0cc..3578244 100644 --- a/Sources/Resolution/NamingServices/CommonNamingService.swift +++ b/Sources/Resolution/NamingServices/CommonNamingService.swift @@ -51,10 +51,15 @@ class CommonNamingService { var node = [UInt8].init(repeating: 0x0, count: 32) if domain.count > 0 { node = domain.split(separator: ".") - .map { Array($0.utf8).sha3(.keccak256) } + .map { Array($0.utf8)} .reversed() - .reduce(node) { return ($0 + $1).sha3(.keccak256) } + .reduce(node) { return self.childHash(parent: $0, label: $1)} } return "0x" + node.toHexString() } + + func childHash(parent: [UInt8], label: [UInt8]) -> [UInt8] { + let childHash = label.sha3(.keccak256) + return (parent + childHash).sha3(.keccak256) + } } diff --git a/Sources/Resolution/NamingServices/ZNS.swift b/Sources/Resolution/NamingServices/ZNS.swift new file mode 100644 index 0000000..d13915d --- /dev/null +++ b/Sources/Resolution/NamingServices/ZNS.swift @@ -0,0 +1,136 @@ +// +// ZNS.swift +// Resolution +// +// Created by Serg Merenkov on 08.09.20. +// Copyright © 2020 Johnny Good. All rights reserved. +// + +import Foundation + +internal class ZNS: CommonNamingService, NamingService { + var network: String + + let registryAddress: String + let registryMap: [String: String] = [ +// "mainnet": "zil1jcgu2wlx6xejqk9jw3aaankw6lsjzeunx2j0jz" + "mainnet": "0x9611c53be6d1b32058b2747bdececed7e1216793" + ] + + init(network: String, providerUrl: String) throws { + guard let registryAddress = registryMap[network] else { + throw ResolutionError.unsupportedNetwork + } + self.network = network + self.registryAddress = registryAddress + super.init(name: "ZNS", providerUrl: providerUrl) + } + + func isSupported(domain: String) -> Bool { + return domain.hasSuffix(".zil") + } + + func isSupportedNetwork() -> Bool { + return registryMap[network] != nil + } + + func owner(domain: String) throws -> String { + let recordAddresses = try self.recordsAddresses(domain: domain) + let (ownerAddress, _ ) = recordAddresses + guard Utillities.isNotEmpty(ownerAddress) else { + throw ResolutionError.unregisteredDomain + } + + return ownerAddress + } + + func addr(domain: String, ticker: String) throws -> String { + return "" + } + + func record(domain: String, key: String) throws -> String { + let records = try self.records(keys: [key], for: domain) + + guard + let record = records[key] else { + throw ResolutionError.recordNotFound + } + + return record + } + + func records(keys: [String], for domain: String) throws -> [String: String] { + guard let records = try self.contract(address: try resolver(domain: domain), keys: keys) as? [String: String] else { + throw ResolutionError.recordNotFound + } + return records + } + + // MARK: - get Resolver + func resolver(domain: String) throws -> String { + let recordAddresses = try self.recordsAddresses(domain: domain) + let (_, resolverAddress ) = recordAddresses + guard Utillities.isNotEmpty(resolverAddress) else { + throw ResolutionError.unspecifiedResolver + } + + return resolverAddress + } + + // MARK: - CommonNamingService + override func childHash(parent: [UInt8], label: [UInt8]) -> [UInt8] { + return (parent + label.sha2(.sha256)).sha2(.sha256) + } + + // MARK: - Helper functions + + private func recordsAddresses(domain: String) throws -> (String, String) { + + if !self.isSupported(domain: domain) { + throw ResolutionError.unsupportedDomain + } + if !self.isSupportedNetwork() { + throw ResolutionError.unsupportedNetwork + } + + let namehash = self.namehash(domain: domain) + let records = try self.contract(address: self.registryAddress, keys: [namehash]) + + guard + let record = records[namehash] as? [String: Any], + let arguments = record["arguments"] as? [Any], arguments.count == 2, + let ownerAddress = arguments[0] as? String, let resolverAddress = arguments[1] as? String + else { + throw ResolutionError.unregisteredDomain + } + + //if (ownerAddress.startsWith('0x')) { + // ownerAddress = toBech32Address(ownerAddress); + //} + + return (ownerAddress, resolverAddress) + } + + private func contract(address: String, keys: [String] = []) throws -> [String: Any] { + + //let contractAddr = contractAddress.startsWith('zil1') ? fromBech32Address(contractAddress) : contractAddress; + + let resolverContract: ContractZNS = + self.buildContract(address: address) + + guard let records = try resolverContract.fetchSubState( + field: "records", + keys: keys + ) as? [String: Any] + else { + throw ResolutionError.unspecifiedResolver + } + + return records + } + + func buildContract(address: String) -> ContractZNS { + return ContractZNS(providerUrl: self.providerUrl, address: address.replacingOccurrences(of: "0x", with: "")) + } + +} diff --git a/Sources/Resolution/Resolution.swift b/Sources/Resolution/Resolution.swift index 9e7a7ff..8d096d8 100644 --- a/Sources/Resolution/Resolution.swift +++ b/Sources/Resolution/Resolution.swift @@ -16,7 +16,8 @@ public class Resolution { init(providerUrl: String, network: String) throws { self.providerUrl = providerUrl let cns = try CNS(network: network, providerUrl: providerUrl) - self.services = [cns] + let zns = try ZNS(network: network, providerUrl: "https://api.zilliqa.com/") + self.services = [cns, zns] } /// Resolves a hash of the `domain` according to https://github.com/ethereum/EIPs/blob/master/EIPS/eip-137.md @@ -25,9 +26,6 @@ public class Resolution { return try getServiceOf(domain: preparedDomain).namehash(domain: preparedDomain) } - public typealias StringResult = (Result) -> Void - public typealias DictionaryResult = (Result<[String: String], ResolutionError>) -> Void - /// Resolves an owner address of a `domain` public func owner(domain: String, completion:@escaping StringResult ) { let preparedDomain = prepare(domain: domain) diff --git a/Tests/ResolutionTests/ResolutionTests.swift b/Tests/ResolutionTests/ResolutionTests.swift index a25a58e..d36d4ce 100644 --- a/Tests/ResolutionTests/ResolutionTests.swift +++ b/Tests/ResolutionTests/ResolutionTests.swift @@ -21,33 +21,51 @@ class ResolutionTests: XCTestCase { } func testNamehash() throws { + // Given // When let firstHashTest = try resolution.namehash(domain: "test.crypto") let secondHashTest = try resolution.namehash(domain: "mongral.crypto") let thirdHashTest = try resolution.namehash(domain: "brad.crypto") + let zilHashTest = try resolution.namehash(domain: "hello.zil") + + // Then assert(firstHashTest == "0xb72f443a17edf4a55f766cf3c83469e6f96494b16823a41a4acb25800f303103") assert(secondHashTest == "0x2038e73f23cbe8c0774c901fbfa77d3ac21c0b13b8f6456f89030d4f13eebba9") assert(thirdHashTest == "0x756e4e998dbffd803c21d23b06cd855cdc7a4b57706c95964a37e24b47c10fc9") + assert(zilHashTest == "0xd7587a5c8caad4941c598440d34f3a454e79889c48e510d13c7c5d1dfc6eab45") } func testGetOwner() throws { // Given - let domainReceived = expectation(description: "Exist domain should be received") + let domainCryptoReceived = expectation(description: "Exist Crypto domain should be received") + let domainZilReceived = expectation(description: "Exist ziliq domain should be received") let unregisteredReceived = expectation(description: "Unregistered domain should be received") var owner = "" + var zilOwner = "" var unregisteredResult: Result! // When resolution.owner(domain: "brad.crypto") { (result) in switch result { case .success(let returnValue): - domainReceived.fulfill() + domainCryptoReceived.fulfill() owner = returnValue case .failure(let error): XCTFail("Expected owner, but got \(error)") } } + + resolution.owner(domain: "brad.zil") { (result) in + switch result { + case .success(let returnValue): + domainZilReceived.fulfill() + zilOwner = returnValue + case .failure(let error): + XCTFail("Expected owner, but got \(error)") + } + } + resolution.owner(domain: "unregistered.crypto") { unregisteredResult = $0 @@ -58,6 +76,7 @@ class ResolutionTests: XCTestCase { // Then assert(owner == "0x8aaD44321A86b170879d7A244c1e8d360c99DdA8".lowercased()) + assert(zilOwner == "0x2d418942dce1afa02d0733a2000c71b371a6ac07".lowercased()) self.checkError(result: unregisteredResult, expectedError: ResolutionError.unregisteredDomain) } @@ -263,13 +282,16 @@ extension ResolutionError: Equatable { return true case ( .unsupportedNetwork, .unsupportedNetwork): return true + case (.unspecifiedResolver, .unspecifiedResolver): + return true // We don't use `default` here on purpose, so we don't forget updating this method on adding new variants. case (.unregisteredDomain, _), (.unsupportedDomain, _), (.unconfiguredDomain, _), (.recordNotFound, _), (.unsupportedNetwork, _), - (.unknownError, _ ): + (.unknownError, _ ), + (.unspecifiedResolver, _): return false } }