432 lines
19 KiB
Swift
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)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|