|
|
|
@ -1,5 +1,5 @@ |
|
|
|
|
// |
|
|
|
|
// File.swift |
|
|
|
|
// QRCodeResolutionCoordinator.swift |
|
|
|
|
// AlphaWallet |
|
|
|
|
// |
|
|
|
|
// Created by Vladyslav Shepitko on 10.09.2020. |
|
|
|
@ -15,6 +15,9 @@ protocol QRCodeResolutionCoordinatorDelegate: class { |
|
|
|
|
func coordinator(_ coordinator: QRCodeResolutionCoordinator, didResolveWalletConnectURL url: WalletConnectURL) |
|
|
|
|
func coordinator(_ coordinator: QRCodeResolutionCoordinator, didResolveString value: String) |
|
|
|
|
func coordinator(_ coordinator: QRCodeResolutionCoordinator, didResolveURL url: URL) |
|
|
|
|
func coordinator(_ coordinator: QRCodeResolutionCoordinator, didResolveJSON json: String) |
|
|
|
|
func coordinator(_ coordinator: QRCodeResolutionCoordinator, didResolveSeedPhase seedPhase: [String]) |
|
|
|
|
func coordinator(_ coordinator: QRCodeResolutionCoordinator, didResolvePrivateKey privateKey: String) |
|
|
|
|
|
|
|
|
|
func didCancel(in coordinator: QRCodeResolutionCoordinator) |
|
|
|
|
} |
|
|
|
@ -44,16 +47,32 @@ private enum ScanQRCodeResolution { |
|
|
|
|
case walletConnect(WalletConnectURL) |
|
|
|
|
case other(String) |
|
|
|
|
case url(URL) |
|
|
|
|
case privateKey(String) |
|
|
|
|
case seedPhase([String]) |
|
|
|
|
case json(String) |
|
|
|
|
|
|
|
|
|
init(rawValue: String) { |
|
|
|
|
if let value = QRCodeValueParser.from(string: rawValue.trimmed) { |
|
|
|
|
let trimmedValue = rawValue.trimmed |
|
|
|
|
|
|
|
|
|
if let value = QRCodeValueParser.from(string: trimmedValue) { |
|
|
|
|
self = .value(value: value) |
|
|
|
|
} else if let rawValue = WalletConnectURL(rawValue) { |
|
|
|
|
self = .walletConnect(rawValue) |
|
|
|
|
} else if let url = URL(string: rawValue) { |
|
|
|
|
} else if let url = WalletConnectURL(rawValue) { |
|
|
|
|
self = .walletConnect(url) |
|
|
|
|
} else if let url = URL(string: trimmedValue), trimmedValue.isValidURL { |
|
|
|
|
self = .url(url) |
|
|
|
|
} else { |
|
|
|
|
self = .other(rawValue) |
|
|
|
|
if trimmedValue.isValidJSON { |
|
|
|
|
self = .json(trimmedValue) |
|
|
|
|
} else if trimmedValue.isPrivateKey { |
|
|
|
|
self = .privateKey(trimmedValue) |
|
|
|
|
} else { |
|
|
|
|
let components = trimmedValue.components(separatedBy: " ") |
|
|
|
|
if components.isEmpty || components.count == 1 { |
|
|
|
|
self = .other(trimmedValue) |
|
|
|
|
} else { |
|
|
|
|
self = .seedPhase(components) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -67,8 +86,8 @@ private enum CheckEIP681Error: Error { |
|
|
|
|
|
|
|
|
|
final class QRCodeResolutionCoordinator: Coordinator { |
|
|
|
|
|
|
|
|
|
private let tokensDatastores: [TokensDataStore] |
|
|
|
|
private let assetDefinitionStore: AssetDefinitionStore |
|
|
|
|
private let tokensDatastores: [TokensDataStore]? |
|
|
|
|
private let assetDefinitionStore: AssetDefinitionStore? |
|
|
|
|
private var skipResolvedCodes: Bool = false |
|
|
|
|
private var navigationController: UINavigationController { |
|
|
|
|
scanQRCodeCoordinator.parentNavigationController |
|
|
|
@ -80,7 +99,7 @@ final class QRCodeResolutionCoordinator: Coordinator { |
|
|
|
|
var coordinators: [Coordinator] = [] |
|
|
|
|
weak var delegate: QRCodeResolutionCoordinatorDelegate? |
|
|
|
|
|
|
|
|
|
init(coordinator: ScanQRCodeCoordinator, tokensDatastores: [TokensDataStore], assetDefinitionStore: AssetDefinitionStore) { |
|
|
|
|
init(coordinator: ScanQRCodeCoordinator, tokensDatastores: [TokensDataStore]?, assetDefinitionStore: AssetDefinitionStore?) { |
|
|
|
|
self.tokensDatastores = tokensDatastores |
|
|
|
|
self.scanQRCodeCoordinator = coordinator |
|
|
|
|
self.assetDefinitionStore = assetDefinitionStore |
|
|
|
@ -107,35 +126,42 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate { |
|
|
|
|
resolveScanResult(result) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private func availableActions(forContract contract: AlphaWallet.Address) -> [ScanQRCodeAction] { |
|
|
|
|
//NOTE: or maybe we need pass though all servers? |
|
|
|
|
guard let tokensDatastore = tokensDatastores?.first(where: { $0.server == rpcServer }) else { |
|
|
|
|
return [] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//I guess if we have token, we shouldn't be able to send to it, or we should? |
|
|
|
|
if tokensDatastore.token(forContract: contract) != nil { |
|
|
|
|
return [.sendToAddress, .watchWallet, .openInEtherscan] |
|
|
|
|
} else { |
|
|
|
|
return [.sendToAddress, .addCustomToken, .watchWallet, .openInEtherscan] |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private func resolveScanResult(_ rawValue: String) { |
|
|
|
|
guard let delegate = delegate else { return } |
|
|
|
|
let rpcServer = self.rpcServer |
|
|
|
|
|
|
|
|
|
switch ScanQRCodeResolution(rawValue: rawValue) { |
|
|
|
|
case .value(let value): |
|
|
|
|
switch value { |
|
|
|
|
case .address(let contract): |
|
|
|
|
let actions: [ScanQRCodeAction] |
|
|
|
|
//NOTE: or maybe we need pass though all servers? |
|
|
|
|
guard let tokensDatastore = tokensDatastores.first(where: { $0.server == rpcServer }) else { return } |
|
|
|
|
|
|
|
|
|
//I guess if we have token, we shouldn't be able to send to it, or we should? |
|
|
|
|
if tokensDatastore.token(forContract: contract) != nil { |
|
|
|
|
actions = [.sendToAddress, .watchWallet, .openInEtherscan] |
|
|
|
|
let actions = availableActions(forContract: contract) |
|
|
|
|
if actions.isEmpty { |
|
|
|
|
delegate.coordinator(self, didResolveAddress: contract, action: .watchWallet) |
|
|
|
|
} else { |
|
|
|
|
actions = [.sendToAddress, .addCustomToken, .watchWallet, .openInEtherscan] |
|
|
|
|
showDidScanWalletAddress(for: actions, completion: { action in |
|
|
|
|
delegate.coordinator(self, didResolveAddress: contract, action: action) |
|
|
|
|
}, cancelCompletion: { |
|
|
|
|
self.skipResolvedCodes = false |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
showDidScanWalletAddress(for: actions, completion: { action in |
|
|
|
|
delegate.coordinator(self, didResolveAddress: contract, action: action) |
|
|
|
|
}, cancelCompletion: { |
|
|
|
|
self.skipResolvedCodes = false |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
case .eip681(let protocolName, let address, let function, let params): |
|
|
|
|
guard let tokensDatastores = tokensDatastores, let assetDefinitionStore = assetDefinitionStore else { return } |
|
|
|
|
let data = CheckEIP681Params(protocolName: protocolName, address: address, functionName: function, params: params, rpcServer: rpcServer) |
|
|
|
|
|
|
|
|
|
self.checkEIP681(data).done { result in |
|
|
|
|
self.checkEIP681(data, tokensDatastores: tokensDatastores, assetDefinitionStore: assetDefinitionStore).done { result in |
|
|
|
|
delegate.coordinator(self, didResolveTransactionType: result.transactionType, token: result.token) |
|
|
|
|
}.cauterize() |
|
|
|
|
} |
|
|
|
@ -150,6 +176,12 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate { |
|
|
|
|
//NOTE: we need to reset flat to false to make sure that next detected QR code will be handled |
|
|
|
|
self.skipResolvedCodes = false |
|
|
|
|
}) |
|
|
|
|
case .json(let value): |
|
|
|
|
delegate.coordinator(self, didResolveJSON: value) |
|
|
|
|
case .privateKey(let value): |
|
|
|
|
delegate.coordinator(self, didResolvePrivateKey: value) |
|
|
|
|
case .seedPhase(let value): |
|
|
|
|
delegate.coordinator(self, didResolveSeedPhase: value) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -204,13 +236,13 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate { |
|
|
|
|
let rpcServer: RPCServer |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private func checkEIP681(_ params: CheckEIP681Params) -> Promise<(transactionType: TransactionType, token: TokenObject)> { |
|
|
|
|
private func checkEIP681(_ params: CheckEIP681Params, tokensDatastores: [TokensDataStore], assetDefinitionStore: AssetDefinitionStore) -> Promise<(transactionType: TransactionType, token: TokenObject)> { |
|
|
|
|
return Eip681Parser(protocolName: params.protocolName, address: params.address, functionName: params.functionName, params: params.params).parse().then { result -> Promise<(transactionType: TransactionType, token: TokenObject)> in |
|
|
|
|
guard let (contract: contract, customServer, recipient, maybeScientificAmountString) = result.parameters else { |
|
|
|
|
return .init(error: CheckEIP681Error.parameterInvalid) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
guard let storage = self.tokensDatastores.first(where: { $0.server == customServer ?? params.rpcServer }) else { |
|
|
|
|
guard let storage = tokensDatastores.first(where: { $0.server == customServer ?? params.rpcServer }) else { |
|
|
|
|
return .init(error: CheckEIP681Error.missingRpcServer) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -224,7 +256,7 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate { |
|
|
|
|
return .value((transactionType, token)) |
|
|
|
|
} else { |
|
|
|
|
return Promise { resolver in |
|
|
|
|
fetchContractDataFor(address: contract, storage: storage, assetDefinitionStore: self.assetDefinitionStore) { result in |
|
|
|
|
fetchContractDataFor(address: contract, storage: storage, assetDefinitionStore: assetDefinitionStore) { result in |
|
|
|
|
switch result { |
|
|
|
|
case .name, .symbol, .balance, .decimals, .nonFungibleTokenComplete, .delegateTokenComplete, .failed: |
|
|
|
|
resolver.reject(CheckEIP681Error.contractInvalid) |
|
|
|
@ -253,6 +285,7 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private extension String { |
|
|
|
|
|
|
|
|
|
var scientificAmountToBigInt: BigInt? { |
|
|
|
|
let numberFormatter = NumberFormatter() |
|
|
|
|
numberFormatter.numberStyle = .decimal |
|
|
|
@ -261,4 +294,20 @@ private extension String { |
|
|
|
|
let amountString = numberFormatter.number(from: self).flatMap { numberFormatter.string(from: $0) } |
|
|
|
|
return amountString.flatMap { BigInt($0) } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var isValidJSON: Bool { |
|
|
|
|
guard let jsonData = self.data(using: .utf8) else { return false } |
|
|
|
|
|
|
|
|
|
return (try? JSONSerialization.jsonObject(with: jsonData)) != nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var isValidURL: Bool { |
|
|
|
|
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) |
|
|
|
|
if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: utf16.count)) { |
|
|
|
|
// it is a link, if the match covers the whole string |
|
|
|
|
return match.range.length == utf16.count |
|
|
|
|
} else { |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|