// // SwapControllerInvestment.swift // Wallet // // Created by Saveliy Stavitsky on 3/25/21. // Copyright © 2021 AM. All rights reserved. // import UIKit // swiftlint:disable:next type_body_length final class SwapControllerInvestment: UIViewController { let service = Swap.Service.Investment() @IBOutlet private weak var scrollView: UIScrollView! @IBOutlet private weak var tokenGiveView: CommonViewCard! @IBOutlet private weak var tokenGetView: CommonViewCard! @IBOutlet private weak var conversionRateStackView: UIStackView! @IBOutlet private weak var forwardConversionRate: UILabel! @IBOutlet private weak var backwardConversionRate: UILabel! @IBOutlet private weak var poolSize: UILabel! @IBOutlet private weak var poolLiquidity: UILabel! @IBOutlet private weak var newPoolWarning: UIView! @IBOutlet private weak var addRemoveLiquidityStackView: UIStackView! @IBOutlet private weak var liquidityView: CommonViewCard! @IBOutlet private weak var segCtrl: UISegmentedControl! @IBOutlet private weak var conversionFieldsStackView: UIStackView! @IBOutlet private weak var amount1TextField: CommonTextField! @IBOutlet private weak var amount2TextField: CommonTextField! @IBOutlet private weak var minimumTextField: CommonTextField! @IBOutlet private weak var countStackView: UIStackView! @IBOutlet private weak var createPoolStackView: UIStackView! @IBOutlet private weak var removeYouGetStackView: UIStackView! @IBOutlet private weak var removeYouGetToken1: UILabel! @IBOutlet private weak var removeYouGetToken2: UILabel! @IBOutlet private weak var removeYouGetToken1TextField: CommonTextField! @IBOutlet private weak var removeYouGetToken2TextField: CommonTextField! @IBOutlet private weak var newPooolFieldsStackView: UIStackView! @IBOutlet private weak var newAmount1TextField: CommonTextField! @IBOutlet private weak var newAmount2TextField: CommonTextField! @IBOutlet private weak var newExchangeRateStackView: UIStackView! @IBOutlet private weak var newExchangeRate: UILabel! @IBOutlet private weak var newBackwardExchangeRate: UILabel! @IBOutlet var swapButton: CommonButtonAction! var startAction: SwapActionType = .add override func viewDidLoad() { super.viewDidLoad() title = L10n.Swap.Investment.title navigationItem.leftBarButtonItem = .pop(self) tabBarItem.title = L10n.Swap.Main.TabBar.title tokenGiveView.action = .cards(collection: nil) tokenGetView.action = .cards(collection: nil) liquidityView.isEnabled = false segCtrl.setTitle(L10n.Swap.Investment.PoolToken.addTitle, forSegmentAt: 0) segCtrl.setTitle(L10n.Swap.Investment.PoolToken.removeTitle, forSegmentAt: 1) segCtrl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected) removeYouGetToken1.isHidden = true removeYouGetToken2.isHidden = true for textField in [amount1TextField, amount2TextField, minimumTextField, newAmount1TextField, newAmount2TextField, removeYouGetToken1TextField, removeYouGetToken2TextField] { textField?.delegate = self textField?.keyboardType = .decimalPad } minimumTextField.precision = 0 swapButton.isHidden = true service.didUpdate = { [weak self] in self?.setupTokensViews() } observers.append(contentsOf: [ Notification.subscribe(name: .didUpdateBalances) { [weak self] _ in guard let self = self else { return } self.service.tokenGive = self.service.swappableBalances.first(where: {$0.symbol == self.service.tokenGive?.symbol}) self.service.tokenGet = self.service.swappableBalances.first(where: {$0.symbol == self.service.tokenGet?.symbol}) self.setupTokensViews() } ]) self.selectActionType(type: self.startAction) } var observers: [AnyObject] = [] deinit { observers.forEach({ Notification.unsubscribe(observer: $0) }) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) Accounts().isLocked = true service.fetch { Alert.error($0) } if self.service.tokenGive != nil && self.service.tokenGet != nil { self.setupTokensViews() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) Accounts().isLocked = false } var selectedReturnAmountIndex: Int? { didSet { if selectedReturnAmountIndex != nil { minimumTextField.resignFirstResponder() removeYouGetToken1TextField.resignFirstResponder() removeYouGetToken2TextField.resignFirstResponder() } } } func setupCountStackView() { countStackView.arrangedSubviews.forEach({ $0.removeFromSuperview() }) countStackView.addArrangedSubview( .button(style: selectedReturnAmountIndex != 0 ? .secondary : .primary, title: "10%", size: .small, action: { [weak self] in self?.selectedReturnAmountIndex = 0 if let amount = self?.service.yourPoolSupply?.amount { self?.service.update(minimum: (amount * 0.1).toReadebleString(precisionMax: 0)) } self?.setupTokensViews() }) ) countStackView.addArrangedSubview( .button(style: selectedReturnAmountIndex != 1 ? .secondary : .primary, title: "25%", size: .small, action: { [weak self] in self?.selectedReturnAmountIndex = 1 if let amount = self?.service.yourPoolSupply?.amount { self?.service.update(minimum: (amount * 0.25).toReadebleString(precisionMax: 0)) } self?.setupTokensViews() }) ) countStackView.addArrangedSubview( .button(style: selectedReturnAmountIndex != 2 ? .secondary : .primary, title: "50%", size: .small, action: { [weak self] in self?.selectedReturnAmountIndex = 2 if let amount = self?.service.yourPoolSupply?.amount { self?.service.update(minimum: (amount * 0.5).toReadebleString(precisionMax: 0)) } self?.setupTokensViews() }) ) countStackView.addArrangedSubview( .button(style: selectedReturnAmountIndex != 3 ? .secondary : .primary, title: "100%", size: .small, action: { [weak self] in self?.selectedReturnAmountIndex = 3 if let amount = self?.service.yourPoolSupply?.amount { self?.service.update(minimum: (amount * 1.0).toReadebleString(precisionMax: 0)) } self?.setupTokensViews() }) ) } // swiftlint:disable:next function_body_length func setupTokensViews() { self.tokenGiveView.action = self.setupTokenCard(isGet: false) self.tokenGetView.action = self.setupTokenCard(isGet: true) conversionRateStackView.isHidden = service.tokenGive == nil || service.tokenGet == nil || service.pool == nil addRemoveLiquidityStackView.isHidden = service.tokenGive == nil || service.tokenGet == nil || service.pool == nil newPoolWarning.isHidden = service.tokenGive == nil || service.tokenGet == nil || service.pool != nil createPoolStackView.isHidden = service.tokenGive == nil || service.tokenGet == nil || service.pool != nil newExchangeRateStackView.isHidden = service.tokenGive == nil || service.tokenGet == nil || service.pool != nil conversionFieldsStackView.isHidden = segCtrl.selectedSegmentIndex == 1 minimumTextField.isHidden = segCtrl.selectedSegmentIndex == 0 removeYouGetStackView.isHidden = segCtrl.selectedSegmentIndex == 0 swapButton.isHidden = true countStackView.superview?.isHidden = true guard let tokenGive = service.tokenGive, let tokenGet = service.tokenGet else { return } removeYouGetToken1TextField.lzPlaceholder = tokenGive.symbol removeYouGetToken1TextField.lzTitle = tokenGive.symbol removeYouGetToken2TextField.lzPlaceholder = tokenGet.symbol removeYouGetToken2TextField.lzTitle = tokenGet.symbol setupCountStackView() let giveImg = UIImageView(image: UIImage.from(token: tokenGive)) giveImg.addHeightWidthConstraints(height: 24, width: 24) newAmount1TextField.accessoryView = giveImg newAmount1TextField.value = service.newAmountSend newAmount1TextField.error = service.newAmountSendError let getImg = UIImageView(image: UIImage.from(token: tokenGet)) getImg.addHeightWidthConstraints(height: 24, width: 24) newAmount2TextField.accessoryView = getImg newAmount2TextField.value = service.newAmountReceive newAmount2TextField.error = service.newAmountReceiveError swapButton.isHidden = false if let newAmountSend = service.newAmountSend.toOptionalDecimal(), let newAmountReceive = service.newAmountReceive.toOptionalDecimal() { newExchangeRateStackView.isHidden = false newExchangeRate.lzText = "\(Decimal(1).toString(precision: tokenGive)) = \((newAmountReceive / newAmountSend).toString(precision: tokenGet))" newBackwardExchangeRate.lzText = "\(Decimal(1).toString(precision: tokenGet)) = \((newAmountSend / newAmountReceive).toString(precision: tokenGive))" } else { newExchangeRateStackView.isHidden = true } guard let pool = service.pool else { swapButton.lzTitle = L10n.Swap.Investment.AddPool.buttonTitle swapButton.isEnabled = !createPoolStackView.isHidden && !newAmount1TextField.value.isEmpty && !newAmount2TextField.value.isEmpty && service.newAmountSend.toDecimal() > 0 && service.newAmountReceive.toDecimal() > 0 && service.newAmountSendError == nil && service.newAmountReceiveError == nil return } forwardConversionRate.lzText = "\(Decimal(1).toString(precision: tokenGive)) ≈ \(pool.forwardExchangeRate.toString(precision: tokenGet))" backwardConversionRate.lzText = "\(Decimal(1).toString(precision: tokenGet)) ≈ \(pool.backwardExchangeRate.toString(precision: tokenGive))" poolSize.lzText = "\(pool.token1?.amount.toString(precision: tokenGive) ?? "") / \(pool.token2?.amount.toString(precision: tokenGet) ?? "")" if let supplyTokens = service.poolSupply { poolLiquidity.lzText = "\(supplyTokens.amount.toString(precision: supplyTokens))" } liquidityView.action = .select( icon: UIImage(), kind: .cards, title: L10n.Common.Card.Token.title, empty: .cardsTokensEmpty(), collection: (service.yourPoolSupply != nil ? [service.yourPoolSupply!] : []) .map({ $0.toMenu() }), selected: service.yourPoolSupply?.toMenu(), completion: { _ in } ) let giveImage = UIImageView(image: UIImage.from(token: tokenGive)) giveImage.addHeightWidthConstraints(height: 24, width: 24) amount1TextField.accessoryView = giveImage amount1TextField.value = service.amountSend amount1TextField.error = service.amountSendError let getImage = UIImageView(image: UIImage.from(token: tokenGet)) getImage.addHeightWidthConstraints(height: 24, width: 24) amount2TextField.accessoryView = getImage amount2TextField.value = service.amountReceive amount2TextField.error = service.amountReceiveError let lImage = UIImageView(image: Asset.tokenSwap.image) lImage.addHeightWidthConstraints(height: 24, width: 24) minimumTextField.accessoryView = lImage minimumTextField.value = service.amountMinimum minimumTextField.error = service.amountMinimumError if let minAmount = service.amountMinimum.toOptionalDecimal(), let poolSupply = service.poolSupply?.amount, let token1 = pool.token1, let token2 = pool.token2, segCtrl.selectedSegmentIndex == 1 { // removeYouGetStackView.isHidden = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { let bottomOffset = CGPoint(x: 0, y: self.scrollView.contentSize.height - self.scrollView.bounds.height + self.scrollView.contentInset.bottom) self.scrollView.setContentOffset(bottomOffset, animated: true) }) removeYouGetToken1.lzText = "≈ \((token1.amount * minAmount / poolSupply).toString(precision: token1))" removeYouGetToken2.lzText = "≈ \((token2.amount * minAmount / poolSupply).toString(precision: token2))" if !removeYouGetToken1TextField.isFirstResponder { var amount = token1.amount * minAmount / poolSupply if token1.isCash { amount = amount.receive(precision: token1.precision) } if amount <= 0 { amount = 0 } removeYouGetToken1TextField.value = "≈\(amount.toReadebleString(precisionMax: token1.precision))" if minAmount == 0 { removeYouGetToken1TextField.value = "≈\(Decimal(0).toReadebleString(precisionMax: token1.precision))" } } if !removeYouGetToken2TextField.isFirstResponder { var amount = token2.amount * minAmount / poolSupply if token2.isCash { amount = amount.receive(precision: token2.precision) } if amount <= 0 { amount = 0 } removeYouGetToken2TextField.value = "≈\(amount.toReadebleString(precisionMax: token2.precision))" if minAmount == 0 { removeYouGetToken2TextField.value = "≈\(Decimal(0).toReadebleString(precisionMax: token2.precision))" } } } else { // removeYouGetStackView.isHidden = true removeYouGetToken1TextField.value = "≈" removeYouGetToken2TextField.value = "≈" } if service.pool == nil { swapButton.lzTitle = L10n.Swap.Investment.AddPool.buttonTitle swapButton.isEnabled = !createPoolStackView.isHidden && !newAmount1TextField.value.isEmpty && !newAmount2TextField.value.isEmpty && service.newAmountSend.toDecimal() > 0 && service.newAmountReceive.toDecimal() > 0 && service.newAmountSendError == nil && service.newAmountReceiveError == nil } else if !conversionFieldsStackView.isHidden { swapButton.lzTitle = L10n.Swap.Investment.PoolToken.addButtonTitle swapButton.isEnabled = !conversionFieldsStackView.isHidden && !amount1TextField.value.isEmpty && !amount2TextField.value.isEmpty && service.amountSend.toDecimal() > 0 && service.amountReceive.toDecimal() > 0 && service.amountSendError == nil && service.amountReceiveError == nil } else if !minimumTextField.isHidden { swapButton.lzTitle = L10n.Swap.Investment.PoolToken.removeButtonTitle countStackView.superview?.isHidden = false swapButton.isEnabled = !minimumTextField.isHidden && !minimumTextField.value.isEmpty && service.amountMinimumError == nil && service.amountMinimum.toDecimal() > 0 } } private func setupTokenCard(isGet: Bool) -> CommonMenuAction { let excludedToken = isGet ? self.service.tokenGive : self.service.tokenGet return .cards(tokens: self.service.swappableBalances.filter({$0 != excludedToken}), selected: isGet ? self.service.tokenGet : self.service.tokenGive, { [weak self] in guard let self else { return } isGet ? (self.service.tokenGet = self.service.swappableBalances.object(menu: $0)) : (self.service.tokenGive = self.service.swappableBalances.object(menu: $0)) self.amount1TextField.precision = self.service.swappableBalances.object(menu: $0)?.precision self.newAmount1TextField.precision = self.service.swappableBalances.object(menu: $0)?.precision self.setupTokensViews() }) } @IBAction private func onChange(_: AnyObject?) { self.setupTokensViews() } @IBAction func swapPressed(_ sender: Any) { view.endEditing(true) guard let tokenGive = service.tokenGive, let tokenGet = service.tokenGet else { return } if service.pool == nil { let views: [UIView] = [ .label( text: L10n.Swap.Investment.CreateConfirmation.description .attributed(style: .regular, size: 14, color: Asset.textCoal.color), numberOfLines: 0 ), .field( title: L10n.Swap.Investment.CreateConfirmation.give, text: service.newAmountSend.toDecimal().toString(max: tokenGive) + "\n" + service.newAmountReceive.toDecimal().toString(max: tokenGet) ) ] Popup.show(title: L10n.Common.Title.confirmation, views: views, submit: L10n.Common.Button.confirm, in: self) { ctrl in AccountViewAuthorize.showGetPrivateKey(in: self) { Loader.show(in: ctrl) self.service.submitCreate(privateKey: $0) { (trxId, lqtoken, error) in ctrl.dismiss(animated: true) Alert.notify(error) let views: [UIView] = [ .field( title: L10n.Swap.Investment.Success.debited, text: (self.service.newAmountSend.toOptionalDecimal()?.toString(max: tokenGive) ?? "") + "\n" + (self.service.newAmountReceive.toOptionalDecimal()?.toString(max: tokenGet) ?? "") ), .field( title: L10n.Swap.Investment.Success.liquidityGet, text: lqtoken ) ] trxId.flatMap { self.showCompletion(transactionId: $0, views: views) } } } } } else if segCtrl.selectedSegmentIndex == 0 && service.pool != nil { let views: [UIView] = [ .label( text: L10n.Swap.Investment.AddConfirmation.description .attributed(style: .regular, size: 14, color: Asset.textCoal.color), numberOfLines: 0 ), .field( title: L10n.Swap.Investment.AddConfirmation.give, text: service.amountSend.toDecimal().toString(max: tokenGive) + "\n" + service.amountReceive.toDecimal().toString(max: tokenGet) ) ] Popup.show(title: L10n.Common.Title.confirmation, views: views, submit: L10n.Common.Button.confirm, in: self) { ctrl in AccountViewAuthorize.showGetPrivateKey(in: self) { Loader.show(in: ctrl) let amountSend = self.service.amountSend.toOptionalDecimal()?.toString(max: tokenGive) let amountReceive = self.service.amountReceive.toOptionalDecimal()?.toString(max: tokenGet) self.service.submitAdd(privateKey: $0) { (trxId, lqtoken, error) in ctrl.dismiss(animated: true) Alert.notify(error) let views: [UIView] = [ .field( title: L10n.Swap.Investment.Success.debited, text: (amountSend ?? "") + "\n" + (amountReceive ?? "") ), .field( title: L10n.Swap.Investment.Success.liquidityGet, text: lqtoken ) ] trxId.flatMap { self.showCompletion(transactionId: $0, views: views) } } } } } else if segCtrl.selectedSegmentIndex == 1 && service.pool != nil { var views: [UIView] = [ .label( text: L10n.Swap.Investment.RemoveConfirmation.description .attributed(style: .regular, size: 14, color: Asset.textCoal.color), numberOfLines: 0 ) ] if let poolSupply = service.poolSupply { views.append( .field( title: L10n.Swap.Investment.RemoveConfirmation.give, text: service.amountMinimum.toDecimal().toString(max: poolSupply) ) ) } if let minAmount = service.amountMinimum.toOptionalDecimal(), let poolSupply = service.poolSupply?.amount, let token1 = service.pool?.token1, let token2 = service.pool?.token2 { views.append( .field( title: L10n.Swap.Investment.RemoveConfirmation.get, text: ("\(self.removeYouGetToken1TextField.value) \(token1.symbol)" + "\n" + "\(self.removeYouGetToken2TextField.value) \(token2.symbol)") .replacingOccurrences(of: "≈", with: "≈ ") ) ) } Popup.show(title: L10n.Common.Title.confirmation, views: views, submit: L10n.Common.Button.confirm, in: self) { ctrl in AccountViewAuthorize.showGetPrivateKey(in: self) { Loader.show(in: ctrl) self.service.submitRemove(privateKey: $0) { (id, error) in // Loader.hide(in: ctrl) ctrl.dismiss(animated: true) Alert.notify(error) var views: [UIView] = [ .field( title: L10n.Swap.Investment.Success.return, text: (self.service.amountMinimum.toOptionalDecimal()?.toString(max: self.service.poolSupply) ?? "") ) ] if let minAmount = self.service.amountMinimum.toOptionalDecimal(), let poolSupply = self.service.poolSupply?.amount, let token1 = self.service.pool?.token1, let token2 = self.service.pool?.token2 { views.append( .field( title: L10n.Swap.Investment.Success.got, text: ("\(self.removeYouGetToken1TextField.value) \(token1.symbol)" + "\n" + "\(self.removeYouGetToken2TextField.value) \(token2.symbol)") .replacingOccurrences(of: "≈", with: "") ) ) } id.flatMap { self.showCompletion(transactionId: $0, views: views) } } } } } } private func showCompletion(transactionId: String, views: [UIView]) { let views = views + [ .field( title: L10n.Swap.Investment.Success.Transaction.id, text: transactionId, rightButtonAvailbale: true, rightButtonImage: Asset.commonCopy.image, rightButtonAction: { Alert.copy(transactionId) } ) ] Popup.show(title: L10n.Swap.Main.Success.title, views: views, submit: L10n.Common.Button.checkOnBloksIO, submitStyle: .outline, cancel: L10n.Common.Button.done, in: self) { ctrl in guard let checkTransactionUrl = URL(transactionId: transactionId) else { return } UIApplication.shared.open(checkTransactionUrl, options: [:], completionHandler: nil) } self.clear() } private func clear() { selectedReturnAmountIndex = nil service.update(send: "") service.update(receive: "") service.update(newReceive: "") service.update(newSend: "") service.update(minimum: "") } func selectActionType(type: SwapActionType) { switch type { case .add: self.segCtrl.selectedSegmentIndex = 0 case .withdraw: self.segCtrl.selectedSegmentIndex = 1 } setupTokensViews() } } extension SwapControllerInvestment: CommonTextFieldDelegate { func textField(_ textField: CommonTextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let newString = (textField.value as NSString).replacingCharacters(in: range, with: string) switch textField { case removeYouGetToken1TextField: if newString.contains("≈") && !newString.starts(with: "≈") { return false } if let precision = service.pool?.token1?.precision, newString.replacingOccurrences(of: "≈", with: "").precision > precision { return false } case removeYouGetToken2TextField: if newString.contains("≈") && !newString.starts(with: "≈") { return false } if let precision = service.pool?.token2?.precision, newString.replacingOccurrences(of: "≈", with: "").precision > precision { return false } default: break } return true } func textFieldDidChange(_ textField: CommonTextField) { switch textField { case amount1TextField: textField.value = textField.value.replacingOccurrences(of: ",", with: ".") let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) service.update(send: textField.value) case amount2TextField: textField.value = textField.value.replacingOccurrences(of: ",", with: ".") let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) service.update(receive: textField.value) case minimumTextField: selectedReturnAmountIndex = nil let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) service.update(minimum: textField.value) case newAmount1TextField: textField.value = textField.value.replacingOccurrences(of: ",", with: ".") let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) service.update(newSend: textField.value) case newAmount2TextField: textField.value = textField.value.replacingOccurrences(of: ",", with: ".") let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) service.update(newReceive: textField.value) case removeYouGetToken1TextField: selectedReturnAmountIndex = nil textField.value = textField.value.replacingOccurrences(of: ",", with: ".") let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) if !textField.value.starts(with: "≈") { textField.value = "≈\(textField.value)" } if let poolSupply = service.poolSupply?.amount, let token1 = service.pool?.token1, let token2 = service.pool?.token2 { let minimum = textField.value.replacingOccurrences(of: "≈", with: "").toDecimal() * poolSupply / token1.amount service.update(minimum: "\(minimum.roundUp(to: 0).toReadebleString(precisionMax: 0))") } case removeYouGetToken2TextField: selectedReturnAmountIndex = nil textField.value = textField.value.replacingOccurrences(of: ",", with: ".") let newPosition = textField.textField.endOfDocument textField.textField.selectedTextRange = textField.textField.textRange(from: newPosition, to: newPosition) if !textField.value.starts(with: "≈") { textField.value = "≈\(textField.value)" } if let poolSupply = service.poolSupply?.amount, let token1 = service.pool?.token1, let token2 = service.pool?.token2 { let minimum = textField.value.replacingOccurrences(of: "≈", with: "").toDecimal() * poolSupply / token2.amount service.update(minimum: "\(minimum.roundUp(to: 0).toReadebleString(precisionMax: 0))") } default: break } } } extension SwapControllerInvestment { enum SwapActionType { case add case withdraw } }