Unstoppable Domains name resolution deprecation/changes #3722

pull/3840/head
Krypto Pank 3 years ago
parent 014654118f
commit d7586cdf82
  1. 28
      AlphaWallet.xcodeproj/project.pbxproj
  2. 8
      AlphaWallet/Accounts/Coordinators/AccountsCoordinator.swift
  3. 10
      AlphaWallet/Accounts/ViewControllers/AccountsViewController.swift
  4. 43
      AlphaWallet/Core/UnstoppableDomains/AddressOrEnsResolution.swift
  5. 88
      AlphaWallet/Core/UnstoppableDomains/DomainResolutionService.swift
  6. 39
      AlphaWallet/Core/UnstoppableDomains/UnstoppableDomainsV1Resolver.swift
  7. 229
      AlphaWallet/Core/UnstoppableDomains/UnstoppableDomainsV2Resolver.swift
  8. 48
      AlphaWallet/Core/Views/AddressOrEnsNameLabel.swift
  9. 1
      AlphaWallet/Settings/Types/Constants+Credentials.swift
  10. 2
      AlphaWallet/Settings/Types/Constants.swift
  11. 64
      AlphaWallet/Tokens/Coordinators/ENSReverseLookupCoordinator.swift
  12. 69
      AlphaWallet/Tokens/Coordinators/GetENSOwnerCoordinator.swift
  13. 20
      AlphaWallet/Tokens/Coordinators/GetWalletNameCoordinator.swift
  14. 14
      AlphaWallet/Transfer/Types/RecipientResolver.swift
  15. 27
      AlphaWallet/Transfer/ViewControllers/RequestViewController.swift
  16. 4
      AlphaWallet/Transfer/ViewModels/TransactionConfirmationViewModel.swift
  17. 2
      AlphaWallet/UI/AddressTextField.swift
  18. 9
      AlphaWallet/Wallet/ViewControllers/RenameWalletViewController.swift

@ -793,7 +793,7 @@
8739BBD426CD2A820045CFED /* TokenCardSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8739BBD326CD2A820045CFED /* TokenCardSelectionCoordinator.swift */; };
873F8063246E8E3E00EEE5EF /* SelectCurrencyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873F8062246E8E3E00EEE5EF /* SelectCurrencyButton.swift */; };
874015BF270DBB4800B3515F /* MimeType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874015BE270DBB4800B3515F /* MimeType+Extension.swift */; };
8743CB50255059780039E469 /* DomainResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8743CB4F255059780039E469 /* DomainResolver.swift */; };
8743CB50255059780039E469 /* UnstoppableDomainsV1Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8743CB4F255059780039E469 /* UnstoppableDomainsV1Resolver.swift */; };
874527D7270B3A25008DB272 /* ArbitrumBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874527D6270B3A25008DB272 /* ArbitrumBridge.swift */; };
874527D9270B3A45008DB272 /* xDaiBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874527D8270B3A45008DB272 /* xDaiBridge.swift */; };
874AF0832603405F00D613A5 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */; };
@ -805,6 +805,9 @@
874D099425EE339700A58EF2 /* TypedDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D099325EE339700A58EF2 /* TypedDataView.swift */; };
874D099625EE33B600A58EF2 /* ScrollableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D099525EE33B600A58EF2 /* ScrollableStackView.swift */; };
874D099825EE33E000A58EF2 /* SelfResizedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D099725EE33E000A58EF2 /* SelfResizedTextView.swift */; };
874D2F7E27A2B1AE005459DA /* DomainResolutionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D2F7D27A2B1AE005459DA /* DomainResolutionService.swift */; };
874D2F8027A2B1D1005459DA /* UnstoppableDomainsV2Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D2F7F27A2B1D1005459DA /* UnstoppableDomainsV2Resolver.swift */; };
874D2F8227A2CBEC005459DA /* AddressOrEnsResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D2F8127A2CBEC005459DA /* AddressOrEnsResolution.swift */; };
874DED0C24C05E88006C8FCE /* TransactionConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874DED0A24C05E88006C8FCE /* TransactionConfirmationViewModel.swift */; };
874DED1524C1BAFF006C8FCE /* SelectTokenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874DED1424C1BAFF006C8FCE /* SelectTokenCoordinator.swift */; };
874DED1724C1BB0E006C8FCE /* SelectTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874DED1624C1BB0E006C8FCE /* SelectTokenViewController.swift */; };
@ -1869,7 +1872,7 @@
8739BBD326CD2A820045CFED /* TokenCardSelectionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenCardSelectionCoordinator.swift; sourceTree = "<group>"; };
873F8062246E8E3E00EEE5EF /* SelectCurrencyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyButton.swift; sourceTree = "<group>"; };
874015BE270DBB4800B3515F /* MimeType+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MimeType+Extension.swift"; sourceTree = "<group>"; };
8743CB4F255059780039E469 /* DomainResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainResolver.swift; sourceTree = "<group>"; };
8743CB4F255059780039E469 /* UnstoppableDomainsV1Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstoppableDomainsV1Resolver.swift; sourceTree = "<group>"; };
874527D6270B3A25008DB272 /* ArbitrumBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitrumBridge.swift; sourceTree = "<group>"; };
874527D8270B3A45008DB272 /* xDaiBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = xDaiBridge.swift; sourceTree = "<group>"; };
874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = "<group>"; };
@ -1881,6 +1884,9 @@
874D099325EE339700A58EF2 /* TypedDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedDataView.swift; sourceTree = "<group>"; };
874D099525EE33B600A58EF2 /* ScrollableStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableStackView.swift; sourceTree = "<group>"; };
874D099725EE33E000A58EF2 /* SelfResizedTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfResizedTextView.swift; sourceTree = "<group>"; };
874D2F7D27A2B1AE005459DA /* DomainResolutionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainResolutionService.swift; sourceTree = "<group>"; };
874D2F7F27A2B1D1005459DA /* UnstoppableDomainsV2Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstoppableDomainsV2Resolver.swift; sourceTree = "<group>"; };
874D2F8127A2CBEC005459DA /* AddressOrEnsResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressOrEnsResolution.swift; sourceTree = "<group>"; };
874DED0A24C05E88006C8FCE /* TransactionConfirmationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionConfirmationViewModel.swift; sourceTree = "<group>"; };
874DED1424C1BAFF006C8FCE /* SelectTokenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTokenCoordinator.swift; sourceTree = "<group>"; };
874DED1624C1BB0E006C8FCE /* SelectTokenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTokenViewController.swift; sourceTree = "<group>"; };
@ -3304,6 +3310,7 @@
29FC9BC31F830880000209CD /* Core */ = {
isa = PBXGroup;
children = (
874D2F7C27A2B14C005459DA /* UnstoppableDomains */,
87D6E5D3278DBB7400B9DEE3 /* Device.swift */,
87D5BBD52727D68C0053E6D2 /* Enjin */,
874BD2B72669766C00E62E02 /* PopularTokens */,
@ -3321,7 +3328,6 @@
5E7C799E4784815CB0202820 /* Core.swift */,
5E7C7543079DF1C7CA998A2D /* Views */,
5E7C70526F00B835220DC0E2 /* Features.swift */,
8743CB4F255059780039E469 /* DomainResolver.swift */,
5E7C7A71851FA227231270BD /* DecodedFunctionCall.swift */,
5E7C77BAC05F2F2417098707 /* ViewControllers */,
5E7C7B097096FC37B68A399B /* EmailList.swift */,
@ -4704,6 +4710,17 @@
path = PopularTokens;
sourceTree = "<group>";
};
874D2F7C27A2B14C005459DA /* UnstoppableDomains */ = {
isa = PBXGroup;
children = (
874D2F7D27A2B1AE005459DA /* DomainResolutionService.swift */,
874D2F7F27A2B1D1005459DA /* UnstoppableDomainsV2Resolver.swift */,
8743CB4F255059780039E469 /* UnstoppableDomainsV1Resolver.swift */,
874D2F8127A2CBEC005459DA /* AddressOrEnsResolution.swift */,
);
path = UnstoppableDomains;
sourceTree = "<group>";
};
87584B4225EFAFB60070063B /* BuyToken */ = {
isa = PBXGroup;
children = (
@ -5799,7 +5816,7 @@
293112121FC4F48400966EEA /* ServiceProvider.swift in Sources */,
2912CD2F1F6A83A100C6CBE3 /* ImportWalletViewController.swift in Sources */,
874DED1724C1BB0E006C8FCE /* SelectTokenViewController.swift in Sources */,
8743CB50255059780039E469 /* DomainResolver.swift in Sources */,
8743CB50255059780039E469 /* UnstoppableDomainsV1Resolver.swift in Sources */,
2963B6AD1F981A96003063C1 /* TransactionAppearance.swift in Sources */,
29850D2B1F6B30FF00791A49 /* TransactionViewController.swift in Sources */,
8739BB6C26CCED670045CFED /* AssetsPageViewModel.swift in Sources */,
@ -5829,6 +5846,7 @@
29AD8A0C1F93FBBF008E10E7 /* Subscribable.swift in Sources */,
295247DF1F8326EF007FDC31 /* AccountViewCell.swift in Sources */,
291F52B71F6B870400B369AB /* CastError.swift in Sources */,
874D2F8227A2CBEC005459DA /* AddressOrEnsResolution.swift in Sources */,
664D11A12007D59F0041A0B0 /* EstimateGasRequest.swift in Sources */,
29DBF2A31F9DBFF400327C60 /* BackupCoordinator.swift in Sources */,
290B2B611F9179880053C83E /* AccountViewModel.swift in Sources */,
@ -5979,6 +5997,7 @@
5E7C7B0367CFB413C6885474 /* GenerateSellMagicLinkViewControllerViewModel.swift in Sources */,
5E7C7692C981580CD32228EB /* ChooseTokenCardTransferModeViewController.swift in Sources */,
5E7C74DBAE43954C185057B3 /* ChooseTokenCardTransferModeViewControllerViewModel.swift in Sources */,
874D2F7E27A2B1AE005459DA /* DomainResolutionService.swift in Sources */,
5E7C71DAA5DAFF764F92587D /* SetTransferTokensCardExpiryDateViewController.swift in Sources */,
879F186026E74543000602F2 /* ToolButtonsBarView.swift in Sources */,
5E7C745C725F3F34037DCC68 /* SetTransferTokensCardExpiryDateViewControllerViewModel.swift in Sources */,
@ -6015,6 +6034,7 @@
878EE944255BC367000210DE /* ShareContentAction.swift in Sources */,
87CC3853273E7980005BE9B8 /* eip155URLDecoder.swift in Sources */,
5E7C7ECE164289A89734B4EF /* LocalesCoordinator.swift in Sources */,
874D2F8027A2B1D1005459DA /* UnstoppableDomainsV2Resolver.swift in Sources */,
879F184826E73CF4000602F2 /* Erc1155TokenInstanceViewController.swift in Sources */,
8750F91724EE5AE100E19DFF /* RecipientResolver.swift in Sources */,
5E7C730C6AEF556AFB9A4B2C /* LocalesViewController.swift in Sources */,

@ -222,10 +222,10 @@ class AccountsCoordinator: Coordinator {
alertController.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel))
alertController.addTextField(configurationHandler: { [weak self] (textField: UITextField!) -> Void in
guard let strongSelf = self else { return }
ENSReverseLookupCoordinator(server: .forResolvingEns).getENSNameFromResolver(forAddress: account) { result in
guard let ensName = result.value else { return }
textField.placeholder = ensName
}
let resolver: DomainResolutionServiceType = DomainResolutionService()
resolver.resolveEns(address: account).done { resolution in
textField.placeholder = resolution.resolution.value
}.cauterize()
let walletNames = strongSelf.config.walletNames
textField.text = walletNames[account]
})

@ -160,13 +160,13 @@ extension AccountsViewController: UITableViewDataSource {
cell.addGestureRecognizer(gesture)
let address = cellViewModel.address
ENSReverseLookupCoordinator(server: .forResolvingEns).getENSNameFromResolver(forAddress: address) { result in
guard let ensName = result.value else { return }
//Cell might have been reused. Check
let resolver: DomainResolutionServiceType = DomainResolutionService()
resolver.resolveEns(address: address).done { resolution in
guard let cellAddress = cell.viewModel?.address, cellAddress.sameContract(as: address) else { return }
cellViewModel.ensName = ensName
cellViewModel.ensName = resolution.resolution.value
cell.configure(viewModel: cellViewModel)
}
}.cauterize()
let subscribableBalance = walletBalanceCoordinator.subscribableWalletBalance(wallet: cellViewModel.wallet)
if let key = cell.balanceSubscribtionKey {

@ -0,0 +1,43 @@
//
// AddressOrEnsResolution.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 27.01.2022.
//
import Foundation
import PromiseKit
enum AddressOrEnsResolution {
case invalidInput
case resolved(AddressOrEnsName?)
var value: String? {
switch self {
case .invalidInput:
return nil
case .resolved(let optional):
return optional?.stringValue
}
}
}
typealias BlockieAndAddressOrEnsResolution = (image: BlockiesImage?, resolution: AddressOrEnsResolution)
protocol DomainResolutionServiceType {
func resolveAddress(string value: String) -> Promise<BlockieAndAddressOrEnsResolution>
func resolveEns(address: AlphaWallet.Address) -> Promise<BlockieAndAddressOrEnsResolution>
}
protocol CachebleAddressResolutionServiceType {
func cachedAddressValue(for input: String) -> AlphaWallet.Address?
}
protocol CachedEnsResolutionServiceType {
func cachedEnsValue(for input: AlphaWallet.Address) -> String?
}
struct ENSLookupKey: Hashable {
let name: String
let server: RPCServer
}

@ -0,0 +1,88 @@
//
// DomainResolutionServiceType.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 27.01.2022.
//
import Foundation
import PromiseKit
class DomainResolutionService: DomainResolutionServiceType {
let server: RPCServer = .forResolvingEns
func resolveAddress(string value: String) -> Promise<BlockieAndAddressOrEnsResolution> {
func resolveBlockieImage(addr: AlphaWallet.Address) -> Promise<BlockieAndAddressOrEnsResolution> {
BlockiesGenerator()
.promise(address: addr)
.map { image -> BlockieAndAddressOrEnsResolution in
return (image, .resolved(.address(addr)))
}.recover { _ -> Promise<BlockieAndAddressOrEnsResolution> in
return .value((nil, .resolved(.address(addr))))
}
}
let getEnsAddressCoordinator = GetENSAddressCoordinator(server: server)
let unstoppableDomainsV1Resolver = UnstoppableDomainsV1Resolver(server: server)
let unstoppableDomainsV2Resolver = UnstoppableDomainsV2Resolver(server: server)
let services: [CachebleAddressResolutionServiceType] = [
getEnsAddressCoordinator,
unstoppableDomainsV2Resolver,
unstoppableDomainsV1Resolver
]
if let cached = services.compactMap({ $0.cachedAddressValue(for: value) }).first {
return resolveBlockieImage(addr: cached)
}
return getEnsAddressCoordinator
.getENSAddressFromResolver(for: value)
.recover { _ -> Promise<AlphaWallet.Address> in
unstoppableDomainsV2Resolver
.resolveAddress(for: value)
.recover { _ -> Promise<AlphaWallet.Address> in
unstoppableDomainsV1Resolver
.resolveAddress(for: value)
}
}.then { addr -> Promise<BlockieAndAddressOrEnsResolution> in
resolveBlockieImage(addr: addr)
}
}
func resolveEns(address: AlphaWallet.Address) -> Promise<BlockieAndAddressOrEnsResolution> {
func resolveBlockieImage(ens: String) -> Promise<BlockieAndAddressOrEnsResolution> {
BlockiesGenerator()
.promise(address: address, ens: ens)
.map { image -> BlockieAndAddressOrEnsResolution in
return (image, .resolved(.ensName(ens)))
}.recover { _ -> Promise<BlockieAndAddressOrEnsResolution> in
return .value((nil, .resolved(.ensName(ens))))
}
}
let ensReverseLookupCoordinator = ENSReverseLookupCoordinator(server: server)
let unstoppableDomainsV2Resolver = UnstoppableDomainsV2Resolver(server: server)
let services: [CachedEnsResolutionServiceType] = [
ensReverseLookupCoordinator,
unstoppableDomainsV2Resolver
]
if let cached = services.compactMap({ $0.cachedEnsValue(for: address) }).first {
return resolveBlockieImage(ens: cached)
}
return ensReverseLookupCoordinator
.getENSNameFromResolver(forAddress: address)
.recover { _ -> Promise<String> in
unstoppableDomainsV2Resolver
.resolveDomain(address: address)
}
.then { ens -> Promise<BlockieAndAddressOrEnsResolution> in
resolveBlockieImage(ens: ens)
}
}
}

@ -1,5 +1,5 @@
//
// DomainResolver.swift
// UnstoppableDomainsV1Resolver.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 02.11.2020.
@ -8,7 +8,6 @@
import Foundation
import UnstoppableDomainsResolution
import PromiseKit
import web3swift
extension Resolution {
convenience init?(server: RPCServer) {
@ -17,13 +16,7 @@ extension Resolution {
}
}
class DomainResolver {
private struct ENSLookupKey: Hashable {
let name: String
let server: RPCServer
}
class UnstoppableDomainsV1Resolver: CachebleAddressResolutionServiceType {
private enum AnyError: Error {
case failureToResolve
case invalidAddress
@ -40,7 +33,12 @@ class DomainResolver {
self.resolution = Resolution(server: server)
}
func resolveAddress(_ input: String) -> Promise<AlphaWallet.Address> {
func cachedAddressValue(for input: String) -> AlphaWallet.Address? {
let node = input.lowercased().nameHash
return cachedResult(forNode: node)
}
func resolveAddress(for input: String) -> Promise<AlphaWallet.Address> {
//if already an address, send back the address
if let value = AlphaWallet.Address(string: input) {
return .value(value)
@ -68,7 +66,6 @@ class DomainResolver {
switch result {
case .success(let value):
if let address = AlphaWallet.Address(string: value) {
seal.fulfill(address)
} else {
seal.reject(AnyError.invalidAddress)
@ -81,27 +78,11 @@ class DomainResolver {
}
private func cachedResult(forNode node: String) -> AlphaWallet.Address? {
return DomainResolver.cache[ENSLookupKey(name: node, server: server)]
return UnstoppableDomainsV1Resolver.cache[ENSLookupKey(name: node, server: server)]
}
private func cache(forNode node: String, result: AlphaWallet.Address) {
DomainResolver.cache[ENSLookupKey(name: node, server: server)] = result
}
}
extension GetENSAddressCoordinator {
func getENSAddressFromResolverPromise(value: String) -> Promise<AlphaWallet.Address> {
return Promise { seal in
GetENSAddressCoordinator(server: server).getENSAddressFromResolver(for: value) { result in
switch result {
case .success(let address):
seal.fulfill(address)
case .failure(let error):
seal.reject(error)
}
}
}
UnstoppableDomainsV1Resolver.cache[ENSLookupKey(name: node, server: server)] = result
}
}

@ -0,0 +1,229 @@
//
// UnstoppableDomainsV2Resolver.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 27.01.2022.
//
import Foundation
import PromiseKit
import Alamofire
import SwiftyJSON
struct UnstoppableDomainsV2ApiError: Error {
var localizedDescription: String
}
class UnstoppableDomainsV2Resolver {
private let server: RPCServer
private static var addressesCache: [ENSLookupKey: AlphaWallet.Address] = [:]
private static var domainsCache: [ENSLookupKey: String] = [:]
init(server: RPCServer) {
self.server = server
}
private static func cachedAddress(forNode node: String, server: RPCServer) -> AlphaWallet.Address? {
return UnstoppableDomainsV2Resolver.addressesCache[ENSLookupKey(name: node, server: server)]
}
private static func cache(forNode node: String, address: AlphaWallet.Address, server: RPCServer) {
UnstoppableDomainsV2Resolver.addressesCache[ENSLookupKey(name: node, server: server)] = address
}
private static func cachedDomain(forNode node: String, server: RPCServer) -> String? {
return UnstoppableDomainsV2Resolver.domainsCache[ENSLookupKey(name: node, server: server)]
}
private static func cache(forNode node: String, domain: String, server: RPCServer) {
UnstoppableDomainsV2Resolver.domainsCache[ENSLookupKey(name: node, server: server)] = domain
}
func resolveAddress(for input: String) -> Promise<AlphaWallet.Address> {
if let value = AlphaWallet.Address(string: input) {
return .value(value)
}
let server = server
let node = input.lowercased().nameHash
if let value = UnstoppableDomainsV2Resolver.cachedAddress(forNode: node, server: server) {
return .value(value)
}
let baseURL = Constants.unstoppableDomainsV2API
guard let url = URL(string: "\(baseURL)/domains/\(input)") else {
return .init(error: UnstoppableDomainsV2ApiError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)"))
}
return Alamofire
.request(url, method: .get, headers: ["Authorization": Constants.Credentials.unstoppableDomainsV2ApiKey])
.responseJSON(queue: .main, options: .allowFragments).map { response -> AlphaWallet.Address in
guard let data = response.response.data, let json = try? JSON(data: data) else {
throw UnstoppableDomainsV2ApiError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)")
}
let value = try AddressResolution.Response(json: json)
if let owner = value.meta.owner {
return owner
} else {
throw UnstoppableDomainsV2ApiError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)")
}
}.get { address in
UnstoppableDomainsV2Resolver.cache(forNode: node, address: address, server: server)
}
}
func resolveDomain(address: AlphaWallet.Address) -> Promise<String> {
let server = server
let node = address.eip55String
if let value = UnstoppableDomainsV2Resolver.cachedDomain(forNode: node, server: server) {
return .value(value)
}
let baseURL = Constants.unstoppableDomainsV2API
guard let url = URL(string: "\(baseURL)/domains/?owners=\(address.eip55String)&sortBy=id&sortDirection=DESC&perPage=50") else {
return .init(error: UnstoppableDomainsV2ApiError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)"))
}
return Alamofire
.request(url, method: .get, headers: ["Authorization": Constants.Credentials.unstoppableDomainsV2ApiKey])
.responseJSON(queue: .main, options: .allowFragments).map { response -> String in
guard let data = response.response.data, let json = try? JSON(data: data) else {
throw UnstoppableDomainsV2ApiError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)")
}
let value = try DomainResolution.Response(json: json)
if let record = value.data.first {
return record.id
} else {
throw UnstoppableDomainsV2ApiError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)")
}
}.get { domain in
UnstoppableDomainsV2Resolver.cache(forNode: node, domain: domain, server: server)
}
}
}
extension UnstoppableDomainsV2Resolver: CachebleAddressResolutionServiceType {
func cachedAddressValue(for input: String) -> AlphaWallet.Address? {
return UnstoppableDomainsV2Resolver.cachedAddress(forNode: input.lowercased().nameHash, server: server)
}
}
extension UnstoppableDomainsV2Resolver: CachedEnsResolutionServiceType {
func cachedEnsValue(for address: AlphaWallet.Address) -> String? {
return UnstoppableDomainsV2Resolver.cachedDomain(forNode: address.eip55String, server: server)
}
}
extension UnstoppableDomainsV2Resolver {
enum DecodingError: Error {
case domainNotFound
case ownerNotFound
case idNotFound
case errorMessageNotFound
case errorCodeNotFound
}
struct AddressResolution {
struct Meta {
let networkId: Int?
let domain: String
let owner: AlphaWallet.Address?
let blockchain: String?
let registry: String?
init(json: JSON) throws {
guard let domain = json["domain"].string else {
throw DecodingError.domainNotFound
}
self.domain = domain
self.owner = json["owner"].string.flatMap { value in
AlphaWallet.Address(uncheckedAgainstNullAddress: value)
}
networkId = json["networkId"].int
blockchain = json["blockchain"].string
registry = json["registry"].string
}
}
struct Response {
let meta: Meta
init(json: JSON) throws {
meta = try Meta(json: json["meta"])
}
}
}
struct DomainResolution {
struct Response {
struct Pagination {
let perPage: Int
let nextStartingAfter: String?
let sortBy: String
let sortDirection: String
let hasMore: Bool
init(json: JSON) {
perPage = json["perPage"].intValue
nextStartingAfter = json["nextStartingAfter"].string
sortBy = json["sortBy"].stringValue
sortDirection = json["sortDirection"].stringValue
hasMore = json["hasMore"].boolValue
}
}
struct ResponseData {
struct Attributes {
let meta: AddressResolution.Meta
init(json: JSON) throws {
meta = try AddressResolution.Meta(json: json["meta"])
}
}
struct Records {
let values: [String: String]
init(json: JSON) throws {
var values: [String: String] = [:]
for key in Constants.unstoppableDomainsRecordKeys {
guard let value = json[key].string else { continue }
values[key] = value
}
self.values = values
}
}
let id: String
let attributes: Attributes
let records: Records
init(json: JSON) throws {
guard let id = json["id"].string else {
throw DecodingError.idNotFound
}
self.id = id
attributes = try Attributes(json: json["attributes"])
records = try Records(json: json["records"])
}
}
let data: [ResponseData]
let meta: Pagination
init(json: JSON) throws {
data = json["data"].arrayValue.compactMap { json in
try? ResponseData(json: json)
}
meta = Pagination(json: json["meta"])
}
}
}
}

@ -11,11 +11,6 @@ import CryptoSwift
class AddressOrEnsNameLabel: UILabel {
enum AddressOrEnsResolution {
case invalidInput
case resolved(AddressOrEnsName?)
}
enum AddressFormat {
case full
case truncateMiddle
@ -120,7 +115,6 @@ class AddressOrEnsNameLabel: UILabel {
}
}
typealias BlockieAndAddressOrEnsResolution = (image: BlockiesImage?, resolution: AddressOrEnsResolution)
// NOTE: caching ids for call `func resolve(_ value: String)` function, for verifying activity state
// adds a new id once function get called, and removes once resolve a value.
private var requestsIdsStore: Set<String> = .init()
@ -134,38 +128,26 @@ class AddressOrEnsNameLabel: UILabel {
if let address = AlphaWallet.Address(string: value) {
inResolvingState = true
firstly {
ENSReverseLookupCoordinator(server: .forResolvingEns).getENSNameFromResolver(forAddress: address)
}.then { ens -> Promise<BlockieAndAddressOrEnsResolution> in
return BlockiesGenerator().promise(address: address, ens: ens).map { image -> BlockieAndAddressOrEnsResolution in
return (image, .resolved(.ensName(ens)))
}.recover { _ -> Promise<BlockieAndAddressOrEnsResolution> in
return .value((nil, .resolved(.ensName(ens))))
DomainResolutionService()
.resolveEns(address: address)
.done { value in
// NOTE: improve loading indicator hidding
self.requestsIdsStore.removeAll()
seal.fulfill(value)
}.catch { _ in
seal.fulfill((nil, .resolved(.none)))
}
}.done { value in
// NOTE: improve loading indicator hidding
self.requestsIdsStore.removeAll()
seal.fulfill(value)
}.catch { _ in
seal.fulfill((nil, .resolved(.none)))
}
} else if value.contains(".") {
inResolvingState = true
GetENSAddressCoordinator(server: .forResolvingEns).getENSAddressFromResolverPromise(value: value).recover { _ -> Promise<AlphaWallet.Address> in
DomainResolver(server: .forResolvingEns).resolveAddress(value)
}.then { addr -> Promise<BlockieAndAddressOrEnsResolution> in
return BlockiesGenerator().promise(address: addr).map { image -> BlockieAndAddressOrEnsResolution in
return (image, .resolved(.address(addr)))
}.recover { _ -> Promise<BlockieAndAddressOrEnsResolution> in
return .value((nil, .resolved(.address(addr))))
DomainResolutionService()
.resolveAddress(string: value)
.done { value in
self.requestsIdsStore.removeAll()
seal.fulfill(value)
}.catch { _ in
seal.fulfill((nil, .resolved(.none)))
}
}.done { value in
self.requestsIdsStore.removeAll()
seal.fulfill(value)
}.catch { _ in
seal.fulfill((nil, .resolved(.none)))
}
} else {
seal.fulfill((nil, .resolved(.none)))
}

@ -16,5 +16,6 @@ extension Constants {
static let enjinUserName = "vlad_shepitko@outlook.com"
static let enjinUserPassword: String = "wf@qJPz75CL9Tw$"
static let walletConnectApiKey = "6eb1175ea3e57209d8f8f39bd4213691"
static let unstoppableDomainsV2ApiKey = "Bearer rLuujk_dLBN-JDE6Xl8QSCg-FeIouRKM"
}
}

@ -82,6 +82,8 @@ public struct Constants {
//UEFA 721 balances function hash
static let balances165Hash721Ticket = "0xc84aae17"
public static let unstoppableDomainsV2API = "https://unstoppabledomains.g.alchemy.com"
public static let unstoppableDomainsRecordKeys = ["crypto.MATIC.version.MATIC.address", "crypto.ETH.address", "crypto.MATIC.version.ERC20.address"]
//OpenSea links for erc721 assets
public static let openseaAPI = "https://api.opensea.io/"
public static let openseaRinkebyAPI = "https://rinkeby-api.opensea.io/"

@ -8,72 +8,54 @@ import PromiseKit
//This class performs a ENS reverse lookup figure out Ethereum address from a given ENS name and then forward resolves the ENS name (look up Ethereum address from ENS name) to verify it. This is necessary because:
// (quoted from https://docs.ens.domains/dapp-developer-guide/resolving-names)
// > "ENS does not enforce the accuracy of reverse records - for instance, anyone may claim that the name for their address is 'alice.eth'. To be certain that the claim is accurate, you must always perform a forward resolution for the returned name and check it matches the original address."
class ENSReverseLookupCoordinator {
private struct ENSLookupKey: Hashable {
let name: String
let server: RPCServer
}
struct ENSReverseLookupCoordinator: CachedEnsResolutionServiceType {
private static var resultsCache = [ENSLookupKey: String]()
private var toStartResolvingEnsNameTimer: Timer?
private let server: RPCServer
init(server: RPCServer) {
self.server = server
}
func getENSNameFromResolver(forAddress input: AlphaWallet.Address) -> Promise<String> {
return Promise<String> { seal in
getENSNameFromResolver(forAddress: input) { result in
switch result {
case .failure(let error):
seal.reject(error)
case .success(let value):
seal.fulfill(value)
}
}
}
func cachedEnsValue(for input: AlphaWallet.Address) -> String? {
let node = "\(input.eip55String.drop0x).addr.reverse".lowercased().nameHash
return cachedResult(forNode: node)
}
//TODO make calls from multiple callers at the same time for the same address more efficient
func getENSNameFromResolver(
forAddress input: AlphaWallet.Address,
completion: @escaping (AWResult<String, AnyError>) -> Void
) {
func getENSNameFromResolver(forAddress input: AlphaWallet.Address) -> Promise<String> {
let node = "\(input.eip55String.drop0x).addr.reverse".lowercased().nameHash
if let cachedResult = cachedResult(forNode: node) {
completion(.success(cachedResult))
return
return .value(cachedResult)
}
let function = GetENSResolverEncode()
callSmartContract(withServer: server, contract: server.ensRegistrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).done { result in
let server = server
return callSmartContract(withServer: server, contract: server.ensRegistrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).then { result -> Promise<String> in
if let resolver = result["0"] as? EthereumAddress {
if Constants.nullAddress.sameContract(as: resolver) {
completion(.failure(AnyError(Web3Error(description: "Null address returned"))))
return .init(error: AnyError(Web3Error(description: "Null address returned")))
} else {
let function = ENSReverseLookupEncode()
callSmartContract(withServer: self.server, contract: AlphaWallet.Address(address: resolver), functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).done { result in
return callSmartContract(withServer: server, contract: AlphaWallet.Address(address: resolver), functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).then { result -> Promise<String> in
guard let ensName = result["0"] as? String, ensName.contains(".") else {
completion(.failure(AnyError(Web3Error(description: "Incorrect data output from ENS resolver"))))
return
return .init(error: AnyError(Web3Error(description: "Incorrect data output from ENS resolver")))
}
GetENSAddressCoordinator(server: self.server).getENSAddressFromResolver(for: ensName) { result in
if let addressFromForwardResolution = result.value, input == addressFromForwardResolution {
self.cache(forNode: node, result: ensName)
completion(.success(ensName))
} else {
completion(.failure(AnyError(Web3Error(description: "Forward resolution of ENS name found by reverse look up doesn't match"))))
return GetENSAddressCoordinator(server: server)
.getENSAddressFromResolver(for: ensName)
.map { address -> String in
if input == address {
ENSReverseLookupCoordinator.cache(forNode: node, result: ensName, server: server)
return ensName
} else {
throw AnyError(Web3Error(description: "Forward resolution of ENS name found by reverse look up doesn't match"))
}
}
}
}.cauterize()
}
}
} else {
completion(.failure(AnyError(Web3Error(description: "Error extracting result from \(self.server.ensRegistrarContract).\(function.name)()"))))
return .init(error: AnyError(Web3Error(description: "Error extracting result from \(server.ensRegistrarContract).\(function.name)()")))
}
}.catch {
completion(.failure(AnyError($0)))
}
}
@ -81,7 +63,7 @@ class ENSReverseLookupCoordinator {
return ENSReverseLookupCoordinator.resultsCache[ENSLookupKey(name: node, server: server)]
}
private func cache(forNode node: String, result: String) {
private static func cache(forNode node: String, result: String, server: RPCServer) {
ENSReverseLookupCoordinator.resultsCache[ENSLookupKey(name: node, server: server)] = result
}
}

@ -5,6 +5,7 @@ import Foundation
import CryptoSwift
import Result
import web3swift
import PromiseKit
//https://github.com/ethereum/EIPs/blob/master/EIPS/eip-137.md
extension String {
@ -20,90 +21,64 @@ extension String {
}
}
class GetENSAddressCoordinator {
private struct ENSLookupKey: Hashable {
let name: String
let server: RPCServer
}
private static var resultsCache = [ENSLookupKey: AlphaWallet.Address]()
private static let DELAY_AFTER_STOP_TYPING_TO_START_RESOLVING_ENS_NAME = TimeInterval(0.5)
class GetENSAddressCoordinator: CachebleAddressResolutionServiceType {
private var toStartResolvingEnsNameTimer: Timer?
private static var resultsCache: [ENSLookupKey: AlphaWallet.Address] = [:]
private (set) var server: RPCServer
init(server: RPCServer) {
self.server = server
}
func getENSAddressFromResolver(
for input: String,
completion: @escaping (Result<AlphaWallet.Address, AnyError>) -> Void
) {
func cachedAddressValue(for input: String) -> AlphaWallet.Address? {
let node = input.lowercased().nameHash
return cachedResult(forNode: node)
}
func getENSAddressFromResolver(for input: String) -> Promise<AlphaWallet.Address> {
//if already an address, send back the address
if let ethAddress = AlphaWallet.Address(string: input) {
completion(.success(ethAddress))
return
return .value(ethAddress)
}
//if it does not contain .eth, then it is not a valid ens name
if !input.contains(".") {
completion(.failure(AnyError(Web3Error(description: "Invalid ENS Name"))))
return
return .init(error: AnyError(Web3Error(description: "Invalid ENS Name")))
}
let node = input.lowercased().nameHash
if let cachedResult = cachedResult(forNode: node) {
completion(.success(cachedResult))
return
return .value(cachedResult)
}
let function = GetENSResolverEncode()
callSmartContract(withServer: server, contract: server.ensRegistrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).done { result in
let server = server
return callSmartContract(withServer: server, contract: server.ensRegistrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).then { result -> Promise<AlphaWallet.Address> in
//if null address is returned (as 0) we count it as invalid
//this is because it is not assigned to an ENS and puts the user in danger of sending funds to null
if let resolver = result["0"] as? EthereumAddress {
if Constants.nullAddress.sameContract(as: resolver) {
completion(.failure(AnyError(Web3Error(description: "Null address returned"))))
return .init(error: AnyError(Web3Error(description: "Null address returned")))
} else {
let function = GetENSRecordFromResolverEncode()
callSmartContract(withServer: self.server, contract: AlphaWallet.Address(address: resolver), functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).done { result in
return callSmartContract(withServer: server, contract: AlphaWallet.Address(address: resolver), functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).map { result in
if let ensAddress = result["0"] as? EthereumAddress {
if Constants.nullAddress.sameContract(as: ensAddress) {
completion(.failure(AnyError(Web3Error(description: "Null address returned"))))
throw AnyError(Web3Error(description: "Null address returned"))
} else {
//Retain self because it's useful to cache the results even if we don't immediately need it now
let adress = AlphaWallet.Address(address: ensAddress)
self.cache(forNode: node, result: adress)
completion(.success(adress))
GetENSAddressCoordinator.cache(forNode: node, result: adress, server: server)
return adress
}
} else {
completion(.failure(AnyError(Web3Error(description: "Incorrect data output from ENS resolver"))))
throw AnyError(Web3Error(description: "Incorrect data output from ENS resolver"))
}
}.cauterize()
}
}
} else {
completion(.failure(AnyError(Web3Error(description: "Error extracting result from \(self.server.ensRegistrarContract).\(function.name)()"))))
}
}.catch {
completion(.failure(AnyError($0)))
}
}
func queueGetENSOwner(for input: String, completion: @escaping (Result<AlphaWallet.Address, AnyError>) -> Void) {
let node = input.lowercased().nameHash
if let cachedResult = cachedResult(forNode: node) {
completion(.success(cachedResult))
return
}
toStartResolvingEnsNameTimer?.invalidate()
toStartResolvingEnsNameTimer = Timer.scheduledTimer(withTimeInterval: GetENSAddressCoordinator.DELAY_AFTER_STOP_TYPING_TO_START_RESOLVING_ENS_NAME, repeats: false) { _ in
//Retain self because it's useful to cache the results even if we don't immediately need it now
self.getENSAddressFromResolver(for: input) { result in
completion(result)
return .init(error: AnyError(Web3Error(description: "Error extracting result from \(server.ensRegistrarContract).\(function.name)()")))
}
}
}
@ -112,7 +87,7 @@ class GetENSAddressCoordinator {
return GetENSAddressCoordinator.resultsCache[ENSLookupKey(name: node, server: server)]
}
private func cache(forNode node: String, result: AlphaWallet.Address) {
private static func cache(forNode node: String, result: AlphaWallet.Address, server: RPCServer) {
GetENSAddressCoordinator.resultsCache[ENSLookupKey(name: node, server: server)] = result
}
}

@ -6,18 +6,22 @@ import PromiseKit
//Use the wallet name which the user has set, otherwise fallback to ENS, if available
class GetWalletNameCoordinator {
private let config: Config
private let resolver = ENSReverseLookupCoordinator(server: .forResolvingEns)
private let resolver: DomainResolutionServiceType = DomainResolutionService()
init(config: Config) {
self.config = config
}
func getName(forAddress address: AlphaWallet.Address) -> Promise<String?> {
Promise { seal in
if let walletName = config.walletNames[address] {
seal.fulfill(walletName)
} else {
resolver.getENSNameFromResolver(forAddress: address) { result in
seal.fulfill(result.value)
func getName(forAddress address: AlphaWallet.Address) -> Promise<String> {
struct ResolveEnsError: Error {}
if let walletName = config.walletNames[address] {
return .value(walletName)
} else {
return resolver.resolveEns(address: address).map { result in
if let value = result.resolution.value {
return value
} else {
throw ResolveEnsError()
}
}
}

@ -16,23 +16,29 @@ class RecipientResolver {
let address: AlphaWallet.Address?
var ensName: String?
var hasResolvedESNName: Bool {
var hasResolvedEnsName: Bool {
if let value = ensName {
return !value.trimmed.isEmpty
}
return false
}
private let resolver: DomainResolutionServiceType = DomainResolutionService()
init(address: AlphaWallet.Address?) {
self.address = address
}
func resolve(completion: @escaping () -> Void) {
guard let address = address else { return }
ENSReverseLookupCoordinator(server: .forResolvingEns).getENSNameFromResolver(forAddress: address) { [weak self] result in
resolver.resolveEns(address: address).done { [weak self] result in
guard let strongSelf = self else { return }
strongSelf.ensName = result.resolution.value
completion()
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.ensName = result.value
strongSelf.ensName = nil
completion()
}
}

@ -174,17 +174,24 @@ class RequestViewController: UIViewController {
private func resolveEns() {
let address = viewModel.myAddress
ENSReverseLookupCoordinator(server: .forResolvingEns).getENSNameFromResolver(forAddress: address) { [weak self] result in
guard let strongSelf = self else { return }
if let ensName = result.value {
let resolver: DomainResolutionServiceType = DomainResolutionService()
resolver.resolveEns(address: address).done { [weak self] result in
guard let strongSelf = self else { return }
if let ensName = result.resolution.value {
strongSelf.ensLabel.text = ensName
strongSelf.ensContainerView.isHidden = false
strongSelf.ensContainerView.cornerRadius = strongSelf.ensContainerView.frame.size.height / 2
} else {
strongSelf.ensLabel.text = nil
strongSelf.ensContainerView.isHidden = true
}
}
strongSelf.ensContainerView.isHidden = false
strongSelf.ensContainerView.cornerRadius = strongSelf.ensContainerView.frame.size.height / 2
} else {
strongSelf.ensLabel.text = nil
strongSelf.ensContainerView.isHidden = true
}
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.ensLabel.text = nil
strongSelf.ensContainerView.isHidden = true
}
}
@objc func textFieldDidChange(_ textField: UITextField) {

@ -249,7 +249,7 @@ extension TransactionConfirmationViewModel {
case .address:
return false
case .ens:
return !recipientResolver.hasResolvedESNName
return !recipientResolver.hasResolvedEnsName
}
} else {
return true
@ -543,7 +543,7 @@ extension TransactionConfirmationViewModel {
case .address:
return false
case .ens:
return !recipientResolver.hasResolvedESNName
return !recipientResolver.hasResolvedEnsName
}
} else {
return true

@ -365,7 +365,7 @@ extension AddressTextField: UITextFieldDelegate {
}.cauterize()
}
private func addressOrEnsNameDidResolve(_ response: AddressOrEnsNameLabel.BlockieAndAddressOrEnsResolution, whileTextWasPaste: Bool) {
private func addressOrEnsNameDidResolve(_ response: BlockieAndAddressOrEnsResolution, whileTextWasPaste: Bool) {
guard value == textField.text?.trimmed else {
return
}

@ -128,11 +128,10 @@ class RenameWalletViewController: UIViewController {
}
private func fulfillTextField(account: AlphaWallet.Address) {
let serverToResolveEns = RPCServer.main
ENSReverseLookupCoordinator(server: serverToResolveEns).getENSNameFromResolver(forAddress: account) { result in
guard let ensName = result.value else { return }
self.nameTextField.textField.placeholder = ensName
}
let resolver: DomainResolutionServiceType = DomainResolutionService()
resolver.resolveEns(address: account).done { resolution in
self.nameTextField.textField.placeholder = resolution.resolution.value
}.cauterize()
let walletNames = config.walletNames
nameTextField.textField.text = walletNames[account]

Loading…
Cancel
Save