lnd-demo-app/wallet/ViewControllers/ChannelsVC.swift
2023-06-08 09:36:06 +03:00

432 lines
19 KiB
Swift

//
// ChannelsVC.swift
// wallet
//
// Created by Adriana Epure on 22.08.2022.
// Copyright © 2022 Jason. All rights reserved.
//
import UIKit
class ChannelsVC: CustomViewController<ChannelsViewModel> {
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<UITouch>, 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)
}
}
}
}