// // ChannelsVC.swift // wallet // // Created by Adriana Epure on 22.08.2022. // Copyright © 2022 Jason. All rights reserved. // import UIKit class ChannelsVC: CustomViewController { private let nodeKeyPlaceholder: String = "Public Node Key ..." @IBOutlet weak var selectedListControl: UISegmentedControl! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var channelsBtnsStack: UIStackView! @IBOutlet weak var availableBalanceLbl: UILabel! //MARK: Open Channel Outlets @IBOutlet weak var newStackView: UIStackView! @IBOutlet weak var newBtn: UIButton! @IBOutlet weak var openAddressTextView: UITextView! @IBOutlet weak var openBtn: UIButton! @IBOutlet weak var cancelOpen: UIButton! @IBOutlet weak var localFundingStepper: UIStepper! @IBOutlet weak var pushAmountStepper: UIStepper! @IBOutlet weak var localFundingAmount: UITextField! @IBOutlet weak var closeAddressTextField: UITextField! @IBOutlet weak var hostAddress: UITextField! @IBOutlet weak var portTextField: UITextField! @IBOutlet weak var pushSatAmount: UITextField! lazy var refreshControl: UIRefreshControl = { let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(refreshData(_:)), for: .valueChanged) refreshControl.tintColor = UIColor.gray return refreshControl }() override func viewDidLoad() { super.viewDidLoad() title = "Channels" self.selectedListControl.selectedSegmentIndex = 0 self.selectedListControl.sendActions(for: .valueChanged) tableView.delegate = self tableView.dataSource = self } override func viewModelDidLoad() { viewModel.channels.observe = { [weak self] channels in self?.tableView.reloadData() } viewModel.pendingChannels.observe = { [weak self] channels in self?.tableView.reloadData() } viewModel.closedChannels.observe = { [weak self] channels in self?.tableView.reloadData() } viewModel.peers.observe = { [weak self] peers in self?.tableView.reloadData() } viewModel.walletBalance.observe = { [weak self] walletBalance in self?.availableBalanceLbl.text = String(walletBalance.total) } viewModel.load() viewModel.getWalletBalance() self.tableView.addSubview(self.refreshControl) } @objc private func refreshData(_ sender: Any) { // Fetch Weather Data refreshList() self.tableView.reloadData() refreshControl.endRefreshing() } //MARK: - New Channel @IBAction func didPressNewBtn(_ sender: UIButton) { openAddressTextView.text = nodeKeyPlaceholder openAddressTextView.textColor = UIColor.lightGray openAddressTextView.delegate = self openAddressTextView.layer.borderColor = UIColor.lightGray.cgColor openAddressTextView.layer.borderWidth = 1 closeAddressTextField.delegate = self localFundingAmount.delegate = self pushSatAmount.delegate = self showHideViews(isNew: true, isFinished: false) openAddressTextView.text = "0245fc5e867abb5b83ead35b50dc5013dd358b9f3eb48c02f5e1cc9fc675039359" } @IBAction func didChangePushSatStepper(_ sender: UIStepper) { pushSatAmount.text = String(String(format: "%.f", sender.value)) toggleOpenBtn() } @IBAction func didChangeLocalFundingStepper(_ sender: UIStepper) { localFundingAmount.text = String(String(format: "%.f", sender.value)) toggleOpenBtn() } @IBAction func didPressOpenBtn(_ sender: UIButton) { self.selectedListControl.selectedSegmentIndex = 0 self.selectedListControl.sendActions(for: .valueChanged) if let channelAddress = openAddressTextView.text{ guard let pubKey = openAddressTextView.text else { return } guard let host = hostAddress.text else { return } guard let port = UInt(portTextField.text ?? "") else { return } guard let funding = Int(localFundingAmount.text ?? "") else { return } guard let push = Int(pushSatAmount.text ?? "") else { return } debugPrint("Channel Address ", channelAddress) viewModel.openChannel(nodePubKey: pubKey, hostAddress: host, port: port, pushSat: push, localFunding: funding) { response in self.showAlert(title: "Channel Open", errorMsg: "Channel Open Request Sent") } onFailure: { error in self.showAlert(title: "Channel Open", errorMsg: "Channel Open Request Failed") } } showHideViews(isNew: true, isFinished: true) } @IBAction func didPressCancelOpenBtn(_ sender: UIButton) { showHideViews(isNew: !newStackView.isHidden, isFinished: true) } //MARK: - Selection @IBAction func didChangeSelection(_ sender: Any) { refreshList() tableView.reloadData() } } extension ChannelsVC{ func refreshList(){ switch selectedListControl.selectedSegmentIndex{ case 0: viewModel.listChannels() case 1: viewModel.listClosedChannels() case 2, 3: viewModel.listPendingChannels() default: viewModel.listPeers() } } func toggleOpenBtn(){ openBtn.isHidden = true let isNodeKeyFilled = !openAddressTextView.text.isEmpty && openAddressTextView.textColor != UIColor.lightGray let isCloseAddressFilled = !(closeAddressTextField.text?.isEmpty ?? true) guard let localFundingText = localFundingAmount.text else { return } guard let pushSatText = pushSatAmount.text else { return } openBtn.isHidden = !(isNodeKeyFilled && Int(localFundingText) ?? 0 > 0 && Int(pushSatText) ?? 0 > 0) } func showHideViews(isNew: Bool, isFinished: Bool){ channelsBtnsStack.isHidden = !isFinished newStackView.isHidden = isFinished || (!isFinished && !isNew) tableView.isHidden = !isFinished selectedListControl.isHidden = !isFinished // openBtn.isHidden = true if isFinished{ localFundingAmount.text = "" localFundingAmount.text = "0" openAddressTextView.text = "" } } } //MARK: - List extension ChannelsVC: UITableViewDelegate, UITableViewDataSource{ func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { if selectedListControl.selectedSegmentIndex == 0 { return viewModel.channels.value?[indexPath.row].active ?? false } else { return false } } func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { return "Close" } func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if selectedListControl.selectedSegmentIndex == 0 && editingStyle == .delete{ if let channel = viewModel.channels.value?[indexPath.row]{ debugPrint(channel) let showAlert = UIAlertController(title: "Close Channel", message: "Are you sure you want to close this channel?", preferredStyle: .alert) showAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in self.viewModel.closeChannel(channel: channel) { response in self.showAlert(title: "Closed Channel", description: "Channel Close Request Sent") } onFailure: { error in self.showAlert(title: "Closed Channel", description: "Channel Close Request Failed") } debugPrint("✅✅ Close channel ✅✅") })) showAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { action in self.viewModel.listChannels() })) self.present(showAlert, animated: true, completion: nil) } } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let emptyLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height)) emptyLabel.text = "" emptyLabel.textAlignment = NSTextAlignment.center self.tableView.backgroundView = emptyLabel switch selectedListControl.selectedSegmentIndex{ case 0: if let noOfChannels = viewModel.channels.value?.count{ return noOfChannels }else{ emptyLabel.text = "No channels" self.tableView.separatorStyle = UITableViewCell.SeparatorStyle.none return 0 } case 1: if let noOfClosedChannels = viewModel.closedChannels.value?.count{ return noOfClosedChannels }else{ emptyLabel.text = "No closed channels" self.tableView.separatorStyle = UITableViewCell.SeparatorStyle.none return 0 } case 2: if let noOfPendingChannels = viewModel.pendingChannels.value?.pendingOpenChannels.count { return noOfPendingChannels }else{ emptyLabel.text = "No pending open channels" self.tableView.separatorStyle = UITableViewCell.SeparatorStyle.none return 0 } case 3: if let noOfPendingChannels = viewModel.pendingChannels.value?.waitingCloseChannels.count { return noOfPendingChannels }else{ emptyLabel.text = "No pending closed channels" self.tableView.separatorStyle = UITableViewCell.SeparatorStyle.none return 0 } default: if let noOfPeers = viewModel.peers.value?.count{ return noOfPeers }else{ emptyLabel.text = "No Peers" self.tableView.separatorStyle = UITableViewCell.SeparatorStyle.none return 0 } } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let reuseCellIdentifier = "cellIdentifier" var cell = tableView.dequeueReusableCell(withIdentifier: reuseCellIdentifier) if (!(cell != nil)) { cell = UITableViewCell(style: .subtitle, reuseIdentifier: reuseCellIdentifier) } cell?.textLabel?.numberOfLines = 0 cell?.detailTextLabel?.numberOfLines = 0 switch selectedListControl.selectedSegmentIndex{ case 0: if let channel = viewModel.channels.value?[indexPath.row]{ cell?.textLabel?.text = "Status: \(channel.active ? "active" : "inactive") \nFee per Kw: \(channel.feePerKw) sat \nUptime: \(channel.uptime) " cell?.detailTextLabel?.text = "\nLocal Balance: \(channel.localBalance) sat \nRemote Balance: \(channel.remoteBalance) sat \nUnsettled balance: \(channel.unsettledBalance) sat \nPublic key: \(channel.remotePubkey)" } case 1: if let channel = viewModel.closedChannels.value?[indexPath.row]{ cell?.textLabel?.text = "\nSettled Balance per Kw: \(channel.settledBalance) sat \nCapacity: \(channel.capacity) " cell?.detailTextLabel?.text = "\nOpen by: \(channel.openInitiator) sat \nClosed by: \(channel.closeInitiator) sat \nTime Locked balance: \(channel.timeLockedBalance) sat \nPublic key: \(channel.remotePubkey)" } case 2: if let channel = viewModel.pendingChannels.value?.pendingOpenChannels[indexPath.row] as? Lnrpc_PendingChannelsResponse.PendingOpenChannel{ debugPrint("Channel pending list", channel, indexPath) cell?.textLabel?.text = "\n Pending Open " cell?.detailTextLabel?.text = "\n \nCapacity: \(channel.channel.capacity) \nConfirmation Height \(channel.confirmationHeight) \nLocal Balance: \(channel.channel.localBalance) sat \nRemote Balance: \(channel.channel.remoteBalance) sat \n \(channel.channel.channelPoint)" } case 3: if let channel = viewModel.pendingChannels.value?.waitingCloseChannels[indexPath.row] as? Lnrpc_PendingChannelsResponse.WaitingCloseChannel{ debugPrint("Channel pending list", channel, indexPath) cell?.textLabel?.text = "\n Pending Close " cell?.detailTextLabel?.text = "\n \nCapacity: \(channel.channel.capacity) \nLimbo Balance \(channel.limboBalance) \n\nLocal TX ID\(channel.commitments.localTxid) \n\nRemote TX ID\(channel.commitments.remoteTxid) \n\nLocal Commit Fee\(channel.commitments.localCommitFeeSat) sat \nRemote Commit Fee\(channel.commitments.remoteCommitFeeSat) sat \n\nLocal Balance: \(channel.channel.localBalance) sat \nRemote Balance: \(channel.channel.remoteBalance) sat \n \(channel.channel.channelPoint)" } default: if let peer = viewModel.peers.value?[indexPath.row]{ cell?.textLabel?.text = "Received : \(peer.satRecv) sat \nSent: \(peer.satSent) sat " cell?.detailTextLabel?.text = "\n Host: \(peer.address) \nInbound: \(peer.inbound) \nPing time:\(peer.pingTime) \nPublic key: \(peer.pubKey)" } } return cell! } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if selectedListControl.selectedSegmentIndex == 0{ if let channel = viewModel.channels.value?[indexPath.row]{ self.viewModel.getChannelInfo(id: channel.chanID) { channelInfo in debugPrint(channelInfo) let info = "🕓 Last Update: \(Int64(channelInfo.lastUpdate).getAsDate().format(style: .medium)) \n💰Capacity: \(channelInfo.capacity)sat \n💰 RemoteBalance: \(channel.remoteBalance)sat \n💰Local Balance: \(channel.localBalance)sat \nPoint: \(channelInfo.chanPoint) \n\n✅->>>>>> Node 1 <<<<<<-✅\n\n💰 Base Fee:\(channelInfo.node1Policy.feeBaseMsat)sat\nFee Rate:\(channelInfo.node2Policy.feeRateMilliMsat)msat \nPublic key: \n\(channelInfo.node1Pub) \n\n✅->>>>>> Node 2 <<<<<<-✅\n\n💰 Base Fee:\(channelInfo.node2Policy.feeBaseMsat)sat\nFee Rate:\(channelInfo.node2Policy.feeRateMilliMsat)msat\nPublic key: \n\(channelInfo.node2Pub) " self.showAlert(title: "Channel Info", description: info) } onFailure: { error in debugPrint(error) } } }else{ if let peer = viewModel.peers.value?[indexPath.row]{ self.showAlert(title: "Peer", address: peer.pubKey) } } } } extension ChannelsVC: UITextFieldDelegate{ /** * Called when 'return' key pressed. return NO to ignore. */ func textFieldShouldReturn(_ textField: UITextField) -> Bool { toggleOpenBtn() textField.resignFirstResponder() return true } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if textField == localFundingAmount || textField == pushSatAmount{ let allowedCharacters = CharacterSet.decimalDigits let characterSet = CharacterSet(charactersIn: string) return allowedCharacters.isSuperset(of: characterSet) } return true } /** * Called when the user click on the view (outside the UITextField). */ func touchesBegan(touches: Set, withEvent event: UIEvent?) { self.view.endEditing(true) } } extension ChannelsVC: UITextPasteDelegate { func textPasteConfigurationSupporting(_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, shouldAnimatePasteOf attributedString: NSAttributedString, to textRange: UITextRange) -> Bool { return false } } extension ChannelsVC: UITextViewDelegate{ func textViewDidBeginEditing(_ textView: UITextView) { if textView.textColor == UIColor.lightGray { textView.text = nil textView.textColor = UIColor.black } } func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { textView.text = nodeKeyPlaceholder textView.textColor = UIColor.lightGray } } func textViewDidChange(_ textView: UITextView) { if(textView.text == UIPasteboard.general.string){ } } func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { // Combine the textView text and the replacement text to // create the updated text string let currentText:String = textView.text let updatedText = (currentText as NSString).replacingCharacters(in: range, with: text) // If updated text view will be empty, add the placeholder // and set the cursor to the beginning of the text view if updatedText.isEmpty { textView.text = nodeKeyPlaceholder textView.textColor = UIColor.lightGray textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument) } // Else if the text view's placeholder is showing and the // length of the replacement string is greater than 0, set // the text color to black then set its text to the // replacement string else if textView.textColor == UIColor.lightGray && !text.isEmpty { textView.textColor = UIColor.black textView.text = text }else if text == "\n" { textView.resignFirstResponder() toggleOpenBtn() return false } // For every other case, the text should change with the usual // behavior... else { return true } // ...otherwise return false since the updates have already // been made return false } func textViewDidChangeSelection(_ textView: UITextView) { if self.view.window != nil { if textView.textColor == UIColor.lightGray { textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument) } } } }