lightning.rs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. use axum::extract::State;
  2. use axum::http::StatusCode;
  3. use axum::response::Html;
  4. use maud::html;
  5. use crate::web::handlers::AppState;
  6. use crate::web::templates::{format_sats_as_btc, layout};
  7. pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
  8. let balances = state.node.inner.list_balances();
  9. let channels = state.node.inner.list_channels();
  10. let (num_active_channels, num_inactive_channels) =
  11. channels
  12. .iter()
  13. .fold((0, 0), |(mut active, mut inactive), c| {
  14. if c.is_usable {
  15. active += 1;
  16. } else {
  17. inactive += 1;
  18. }
  19. (active, inactive)
  20. });
  21. let content = if channels.is_empty() {
  22. html! {
  23. h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
  24. // Quick Actions section - matching dashboard style
  25. div class="card" style="margin-bottom: 2rem;" {
  26. h2 { "Quick Actions" }
  27. div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
  28. a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
  29. button class="button-primary" style="width: 100%;" { "Open Channel" }
  30. }
  31. a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
  32. button class="button-primary" style="width: 100%;" { "Create Invoice" }
  33. }
  34. a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
  35. button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
  36. }
  37. }
  38. }
  39. // Balance Information as metric cards
  40. div class="card" {
  41. h2 { "Balance Information" }
  42. div class="metrics-container" {
  43. div class="metric-card" {
  44. div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
  45. div class="metric-label" { "Lightning Balance" }
  46. }
  47. div class="metric-card" {
  48. div class="metric-value" { (format!("{}", num_active_channels + num_inactive_channels)) }
  49. div class="metric-label" { "Total Channels" }
  50. }
  51. div class="metric-card" {
  52. div class="metric-value" { (format!("{}", num_active_channels)) }
  53. div class="metric-label" { "Active Channels" }
  54. }
  55. div class="metric-card" {
  56. div class="metric-value" { (format!("{}", num_inactive_channels)) }
  57. div class="metric-label" { "Inactive Channels" }
  58. }
  59. }
  60. }
  61. div class="card" {
  62. p { "No channels found. Create your first channel to start using Lightning Network." }
  63. }
  64. }
  65. } else {
  66. html! {
  67. h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
  68. // Quick Actions section - matching dashboard style
  69. div class="card" style="margin-bottom: 2rem;" {
  70. h2 { "Quick Actions" }
  71. div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
  72. a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
  73. button class="button-primary" style="width: 100%;" { "Open Channel" }
  74. }
  75. a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
  76. button class="button-primary" style="width: 100%;" { "Create Invoice" }
  77. }
  78. a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
  79. button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
  80. }
  81. }
  82. }
  83. // Balance Information as metric cards
  84. div class="card" {
  85. h2 { "Balance Information" }
  86. div class="metrics-container" {
  87. div class="metric-card" {
  88. div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
  89. div class="metric-label" { "Lightning Balance" }
  90. }
  91. div class="metric-card" {
  92. div class="metric-value" { (format!("{}", num_active_channels + num_inactive_channels)) }
  93. div class="metric-label" { "Total Channels" }
  94. }
  95. div class="metric-card" {
  96. div class="metric-value" { (format!("{}", num_active_channels)) }
  97. div class="metric-label" { "Active Channels" }
  98. }
  99. div class="metric-card" {
  100. div class="metric-value" { (format!("{}", num_inactive_channels)) }
  101. div class="metric-label" { "Inactive Channels" }
  102. }
  103. }
  104. }
  105. div class="card" {
  106. h2 { "Channel Details" }
  107. // Channels list
  108. @for channel in &channels {
  109. div class="channel-item" {
  110. div class="channel-header" {
  111. span class="channel-id" { "Channel ID: " (channel.channel_id.to_string()) }
  112. @if channel.is_usable {
  113. span class="status-badge status-active" { "Active" }
  114. } @else {
  115. span class="status-badge status-inactive" { "Inactive" }
  116. }
  117. }
  118. div class="info-item" {
  119. span class="info-label" { "Counterparty" }
  120. span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel.counterparty_node_id.to_string()) }
  121. }
  122. @if let Some(short_channel_id) = channel.short_channel_id {
  123. div class="info-item" {
  124. span class="info-label" { "Short Channel ID" }
  125. span class="info-value" { (short_channel_id.to_string()) }
  126. }
  127. }
  128. div class="balance-info" {
  129. div class="balance-item" {
  130. div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
  131. div class="balance-label" { "Outbound" }
  132. }
  133. div class="balance-item" {
  134. div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
  135. div class="balance-label" { "Inbound" }
  136. }
  137. div class="balance-item" {
  138. div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
  139. div class="balance-label" { "Total" }
  140. }
  141. }
  142. @if channel.is_usable {
  143. div style="margin-top: 1rem; display: flex; gap: 0.5rem;" {
  144. a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
  145. button style="background: #dc3545;" { "Close Channel" }
  146. }
  147. a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
  148. button style="background: #d63384;" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
  149. }
  150. }
  151. }
  152. }
  153. }
  154. }
  155. }
  156. };
  157. Ok(Html(layout("Lightning", content).into_string()))
  158. }