diff --git a/web3sTests/ENS/ENSOffchainTests.swift b/web3sTests/ENS/ENSOffchainTests.swift index 2c3a336a..42e30b38 100644 --- a/web3sTests/ENS/ENSOffchainTests.swift +++ b/web3sTests/ENS/ENSOffchainTests.swift @@ -68,5 +68,20 @@ class ENSOffchainTests: XCTestCase { XCTFail("Expected ens but failed \(error).") } } + + func testGivenRopstenRegistry_WhenWildcardSupported_AndAddressHasSubdomain_ThenResolvesCorrectly() async { + do { + let nameService = EthereumNameService(client: client!) + + let address = try await nameService.resolve( + ens: "1.offchainexample.eth", + mode: .allowOffchainLookup + ) + + XCTAssertEqual(address, EthereumAddress("0x41563129cdbbd0c5d3e1c86cf9563926b243834d")) + } catch { + XCTFail("Expected ens but failed \(error).") + } + } } diff --git a/web3sTests/ENS/ENSTests.swift b/web3sTests/ENS/ENSTests.swift index 76ba2a36..686150b4 100644 --- a/web3sTests/ENS/ENSTests.swift +++ b/web3sTests/ENS/ENSTests.swift @@ -176,5 +176,35 @@ class ENSTests: XCTestCase { ] ) } + + func testGivenRopstenRegistry_WhenWildcardSupported_AndAddressHasSubdomain_ThenResolvesExampleCorrectly() async { + do { + let nameService = EthereumNameService(client: client!) + + let address = try await nameService.resolve( + ens: "ricmoose.hatch.eth", + mode: .onchain + ) + + XCTAssertEqual(address, EthereumAddress("0x4FaBE0A3a4DDd9968A7b4565184Ad0eFA7BE5411")) + } catch { + XCTFail("Expected ens but failed \(error).") + } + } + + func testGivenRopstenRegistry_WhenWildcardNOTSupported_AndAddressHasSubdomain_ThenFailsResolving() async { + do { + let nameService = EthereumNameService(client: client!) + + _ = try await nameService.resolve( + ens: "1.resolver.eth", + mode: .onchain + ) + + XCTFail("Expected error") + } catch { + XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) + } + } } diff --git a/web3swift/src/ENS/ENSResolver.swift b/web3swift/src/ENS/ENSResolver.swift index ea3576b9..17997195 100644 --- a/web3swift/src/ENS/ENSResolver.swift +++ b/web3swift/src/ENS/ENSResolver.swift @@ -13,6 +13,7 @@ class ENSResolver { let address: EthereumAddress let callResolution: CallResolution private (set) var supportsWildCard: Bool? + private let mustSupportWilcard: Bool private let client: EthereumClientProtocol @@ -20,12 +21,14 @@ class ENSResolver { address: EthereumAddress, client: EthereumClientProtocol, callResolution: CallResolution, - supportsWildCard: Bool? = nil + supportsWildCard: Bool? = nil, + mustSupportWildcard: Bool = false ) { self.address = address self.callResolution = callResolution self.client = client self.supportsWildCard = supportsWildCard + self.mustSupportWilcard = mustSupportWildcard } func resolve( @@ -39,7 +42,12 @@ class ENSResolver { } self.supportsWildCard = wildcardResolution - if wildcardResolution && callResolution.allowsOffchain { + if mustSupportWilcard && !wildcardResolution { + // Wildcard name resolution (ENSIP-10) + throw EthereumNameServiceError.ensUnknown + } + + if wildcardResolution { let response = try await ENSContracts.ENSOffchainResolverFunctions.resolve( contract: address, parameter: .name(name) @@ -73,7 +81,7 @@ class ENSResolver { } self.supportsWildCard = wildcardResolution - if wildcardResolution && callResolution.allowsOffchain { + if wildcardResolution { let response = try await ENSContracts.ENSOffchainResolverFunctions.resolve( contract: self.address, parameter: .address(address) @@ -101,7 +109,7 @@ class ENSResolver { } } - private func supportsWildcard() async throws -> Bool { + func supportsWildcard() async throws -> Bool { try await ERC165(client: client).supportsInterface( contract: address, id: ENSContracts.ENSOffchainResolverFunctions.interfaceId diff --git a/web3swift/src/ENS/EthereumNameService.swift b/web3swift/src/ENS/EthereumNameService.swift index b72645ff..b2ebb110 100644 --- a/web3swift/src/ENS/EthereumNameService.swift +++ b/web3swift/src/ENS/EthereumNameService.swift @@ -39,7 +39,6 @@ protocol EthereumNameServiceProtocol { public enum EthereumNameServiceError: Error, Equatable { case noNetwork - case noResolver case ensUnknown case invalidInput case decodeIssue @@ -72,38 +71,18 @@ public class EthereumNameService: EthereumNameServiceProtocol { return completion(EthereumNameServiceError.noNetwork, nil) } - let function = ENSContracts.ENSRegistryFunctions.resolver( - contract: registryAddress, - parameter: .address(address) - ) - - function.call( - withClient: client, - responseType: ENSContracts.AddressResponse.self, - block: .Latest, - resolution: .noOffchain(failOnExecutionError: true) - ) { (error, response) in - if case .executionError = error { - return completion(.ensUnknown, nil) - } - - guard let resolverAddress = response?.value else { - return completion(EthereumNameServiceError.noResolver, nil) - } - - Task { - let resolver = ENSResolver( - address: resolverAddress, - client: self.client, - callResolution: mode.callResolution(maxRedirects: self.maximumRedirections) + Task { + do { + let resolver = try await getResolver( + for: address, + registryAddress: registryAddress, + mode: mode ) - do { - let name = try await resolver.resolve(address: address) - completion(nil, name) - } catch let error { - completion(error as? EthereumNameServiceError ?? .ensUnknown, nil) - } + let name = try await resolver.resolve(address: address) + completion(nil, name) + } catch let error { + completion(error as? EthereumNameServiceError ?? .ensUnknown, nil) } } } @@ -116,39 +95,23 @@ public class EthereumNameService: EthereumNameServiceProtocol { guard let network = client.network, let registryAddress = self.registryAddress ?? ENSContracts.registryAddress(for: network) else { - return completion(EthereumNameServiceError.noNetwork, nil) - } - let function = ENSContracts.ENSRegistryFunctions.resolver( - contract: registryAddress, - parameter: .name(ens) - ) - - function.call( - withClient: client, - responseType: ENSContracts.AddressResponse.self, - block: .Latest, - resolution: .noOffchain(failOnExecutionError: true) - ) { (error, response) in - if case .executionError = error { - return completion(.ensUnknown, nil) - } - - guard let resolverAddress = response?.value else { - return completion(EthereumNameServiceError.noResolver, nil) - } + return completion(EthereumNameServiceError.noNetwork, nil) + } + Task { + do { + let resolver = try await getResolver( + for: ens, + fullName: ens, + registryAddress: registryAddress, + mode: mode + ) - Task { - let resolver = ENSResolver( - address: resolverAddress, - client: self.client, - callResolution: mode.callResolution(maxRedirects: self.maximumRedirections) + let address = try await resolver.resolve( + name: ens ) - do { - let address = try await resolver.resolve(name: ens) - completion(nil, address) - } catch let error { - completion(error as? EthereumNameServiceError ?? .ensUnknown, nil) - } + completion(nil, address) + } catch let error { + completion(error as? EthereumNameServiceError ?? .ensUnknown, nil) } } } @@ -213,3 +176,82 @@ fileprivate extension ResolutionMode { } } } + + +extension EthereumNameService { + private func getResolver( + for address: EthereumAddress, + registryAddress: EthereumAddress, + mode: ResolutionMode + ) async throws -> ENSResolver { + let function = ENSContracts.ENSRegistryFunctions.resolver( + contract: registryAddress, + parameter: .address(address) + ) + + do { + let resolverAddress = try await function.call( + withClient: client, + responseType: ENSContracts.AddressResponse.self, + block: .Latest, + resolution: .noOffchain(failOnExecutionError: true) + ).value + + return ENSResolver( + address: resolverAddress, + client: client, + callResolution: mode.callResolution(maxRedirects: self.maximumRedirections) + ) + } catch { + throw EthereumNameServiceError.ensUnknown + } + } + + private func getResolver( + for name: String, + fullName: String, + registryAddress: EthereumAddress, + mode: ResolutionMode + ) async throws -> ENSResolver { + let function = ENSContracts.ENSRegistryFunctions.resolver( + contract: registryAddress, + parameter: .name(name) + ) + + do { + let resolverAddress = try await function.call( + withClient: client, + responseType: ENSContracts.AddressResponse.self, + block: .Latest, + resolution: .noOffchain(failOnExecutionError: true) + ).value + + guard resolverAddress != .zero else { + // Wildcard name resolution (ENSIP-10) + let parent = name.split(separator: ".").dropFirst() + + guard parent.count > 1 else { + throw EthereumNameServiceError.ensUnknown + } + + let parentName = parent.joined(separator: ".") + return try await getResolver( + for: parentName, + fullName: fullName, + registryAddress: registryAddress, + mode: mode + ) + } + + return ENSResolver( + address: resolverAddress, + client: client, + callResolution: mode.callResolution(maxRedirects: self.maximumRedirections), + mustSupportWildcard: fullName != name + ) + } catch { + throw error as? EthereumNameServiceError ?? .ensUnknown + } + } + +}