Skip to content

Commit 14dc5c4

Browse files
authored
Merge branch 'master' into fix/node-stopping-bg-payments
2 parents 64e7c96 + 8666c53 commit 14dc5c4

6 files changed

Lines changed: 444 additions & 0 deletions

File tree

app/src/main/java/to/bitkit/repositories/LightningRepo.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import kotlinx.coroutines.withTimeoutOrNull
3232
import org.lightningdevkit.ldknode.Address
3333
import org.lightningdevkit.ldknode.BalanceDetails
3434
import org.lightningdevkit.ldknode.BestBlock
35+
import org.lightningdevkit.ldknode.Bolt11Invoice
3536
import org.lightningdevkit.ldknode.ChannelConfig
3637
import org.lightningdevkit.ldknode.ChannelDataMigration
3738
import org.lightningdevkit.ldknode.ChannelDetails
@@ -1057,6 +1058,23 @@ class LightningRepo @Inject constructor(
10571058
}
10581059
// endregion
10591060

1061+
// region probing
1062+
suspend fun sendProbeForInvoice(bolt11: String, amountSats: ULong? = null): Result<Unit> =
1063+
executeWhenNodeRunning("sendProbeForInvoice") {
1064+
runCatching {
1065+
val invoice = Bolt11Invoice.fromStr(bolt11)
1066+
if (amountSats != null) {
1067+
val amountMsat = amountSats * 1000u
1068+
lightningService.sendProbesUsingAmount(invoice, amountMsat)
1069+
} else {
1070+
lightningService.sendProbes(invoice)
1071+
}
1072+
}.getOrElse {
1073+
Result.failure(it)
1074+
}
1075+
}
1076+
// endregion
1077+
10601078
suspend fun restartNode(): Result<Unit> = withContext(bgDispatcher) {
10611079
Logger.info("Restarting node", context = TAG)
10621080
stop().onFailure {

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,34 @@ class LightningService @Inject constructor(
594594
}
595595
// endregion
596596

597+
// region probing
598+
suspend fun sendProbes(invoice: Bolt11Invoice): Result<Unit> {
599+
val node = this.node ?: throw ServiceError.NodeNotSetup()
600+
601+
return ServiceQueue.LDK.background {
602+
runCatching {
603+
node.bolt11Payment().sendProbes(invoice, null)
604+
Result.success(Unit)
605+
}.getOrElse {
606+
Result.failure(if (it is NodeException) LdkError(it) else it)
607+
}
608+
}
609+
}
610+
611+
suspend fun sendProbesUsingAmount(invoice: Bolt11Invoice, amountMsat: ULong): Result<Unit> {
612+
val node = this.node ?: throw ServiceError.NodeNotSetup()
613+
614+
return ServiceQueue.LDK.background {
615+
runCatching {
616+
node.bolt11Payment().sendProbesUsingAmount(invoice, amountMsat, null)
617+
Result.success(Unit)
618+
}.getOrElse {
619+
Result.failure(if (it is NodeException) LdkError(it) else it)
620+
}
621+
}
622+
}
623+
// endregion
624+
597625
// region utxo selection
598626
suspend fun listSpendableOutputs(): Result<List<SpendableUtxo>> {
599627
val node = this.node ?: throw ServiceError.NodeNotSetup()

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import to.bitkit.ui.screens.scanner.SCAN_REQUEST_KEY
6969
import to.bitkit.ui.screens.settings.DevSettingsScreen
7070
import to.bitkit.ui.screens.settings.FeeSettingsScreen
7171
import to.bitkit.ui.screens.settings.LdkDebugScreen
72+
import to.bitkit.ui.screens.settings.ProbingToolScreen
7273
import to.bitkit.ui.screens.shop.ShopIntroScreen
7374
import to.bitkit.ui.screens.shop.shopDiscover.ShopDiscoverScreen
7475
import to.bitkit.ui.screens.shop.shopWebView.ShopWebViewScreen
@@ -890,6 +891,9 @@ private fun NavGraphBuilder.settings(
890891
composableWithDefaultTransitions<Routes.LdkDebug> {
891892
LdkDebugScreen(navController)
892893
}
894+
composableWithDefaultTransitions<Routes.ProbingTool> {
895+
ProbingToolScreen(navController)
896+
}
893897
composableWithDefaultTransitions<Routes.FeeSettings> {
894898
FeeSettingsScreen(navController)
895899
}
@@ -1838,6 +1842,9 @@ sealed interface Routes {
18381842
@Serializable
18391843
data object LdkDebug : Routes
18401844

1845+
@Serializable
1846+
data object ProbingTool : Routes
1847+
18411848
@Serializable
18421849
data object FeeSettings : Routes
18431850

app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ fun DevSettingsScreen(
5252
SettingsButtonRow("Fee Settings") { navController.navigate(Routes.FeeSettings) }
5353
SettingsButtonRow("Channel Orders") { navController.navigate(Routes.ChannelOrdersSettings) }
5454
SettingsButtonRow("LDK Debug") { navController.navigate(Routes.LdkDebug) }
55+
SettingsButtonRow("Probing Tool") { navController.navigate(Routes.ProbingTool) }
5556

5657
SectionHeader("LOGS")
5758
SettingsButtonRow("Logs") { navController.navigate(Routes.Logs) }
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package to.bitkit.ui.screens.settings
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.expandVertically
5+
import androidx.compose.animation.fadeIn
6+
import androidx.compose.animation.fadeOut
7+
import androidx.compose.animation.shrinkVertically
8+
import androidx.compose.foundation.layout.Arrangement
9+
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.PaddingValues
11+
import androidx.compose.foundation.layout.Row
12+
import androidx.compose.foundation.layout.fillMaxWidth
13+
import androidx.compose.foundation.layout.imePadding
14+
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.rememberScrollState
16+
import androidx.compose.foundation.text.KeyboardOptions
17+
import androidx.compose.foundation.verticalScroll
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.runtime.getValue
20+
import androidx.compose.runtime.mutableStateOf
21+
import androidx.compose.runtime.remember
22+
import androidx.compose.runtime.setValue
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.text.input.ImeAction
25+
import androidx.compose.ui.text.input.KeyboardType
26+
import androidx.compose.ui.tooling.preview.Preview
27+
import androidx.compose.ui.unit.dp
28+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
29+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
30+
import androidx.navigation.NavController
31+
import to.bitkit.R
32+
import to.bitkit.ui.components.ButtonSize
33+
import to.bitkit.ui.components.PrimaryButton
34+
import to.bitkit.ui.components.SecondaryButton
35+
import to.bitkit.ui.components.TextInput
36+
import to.bitkit.ui.components.VerticalSpacer
37+
import to.bitkit.ui.components.settings.SectionFooter
38+
import to.bitkit.ui.components.settings.SectionHeader
39+
import to.bitkit.ui.components.settings.SettingsTextButtonRow
40+
import to.bitkit.ui.scaffold.AppTopBar
41+
import to.bitkit.ui.scaffold.DrawerNavIcon
42+
import to.bitkit.ui.scaffold.ScreenColumn
43+
import to.bitkit.ui.theme.AppThemeSurface
44+
import to.bitkit.ui.theme.Colors
45+
import to.bitkit.viewmodels.ProbeResult
46+
import to.bitkit.viewmodels.ProbingToolUiState
47+
import to.bitkit.viewmodels.ProbingToolViewModel
48+
49+
@Composable
50+
fun ProbingToolScreen(
51+
navController: NavController,
52+
viewModel: ProbingToolViewModel = hiltViewModel(),
53+
) {
54+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
55+
56+
ProbingToolContent(
57+
uiState = uiState,
58+
onBackClick = { navController.popBackStack() },
59+
onInvoiceChange = viewModel::updateInvoice,
60+
onAmountChange = viewModel::updateAmountSats,
61+
onPasteInvoice = viewModel::pasteInvoice,
62+
onSendProbe = viewModel::sendProbe,
63+
)
64+
}
65+
66+
@Composable
67+
private fun ProbingToolContent(
68+
uiState: ProbingToolUiState,
69+
onBackClick: () -> Unit,
70+
onInvoiceChange: (String) -> Unit,
71+
onAmountChange: (String) -> Unit,
72+
onPasteInvoice: () -> Unit,
73+
onSendProbe: () -> Unit,
74+
) {
75+
ScreenColumn {
76+
AppTopBar(
77+
titleText = "Probing Tool",
78+
onBackClick = onBackClick,
79+
actions = { DrawerNavIcon() },
80+
)
81+
Column(
82+
modifier = Modifier
83+
.padding(horizontal = 16.dp)
84+
.imePadding()
85+
.verticalScroll(rememberScrollState())
86+
) {
87+
SectionHeader("PROBE INVOICE", padding = PaddingValues(0.dp))
88+
SectionFooter("Enter a Lightning invoice to probe the payment route")
89+
90+
TextInput(
91+
value = uiState.invoice,
92+
onValueChange = onInvoiceChange,
93+
placeholder = "lnbc...",
94+
singleLine = false,
95+
modifier = Modifier
96+
.fillMaxWidth()
97+
.padding(vertical = 8.dp),
98+
)
99+
100+
Row(
101+
horizontalArrangement = Arrangement.spacedBy(8.dp),
102+
modifier = Modifier.fillMaxWidth(),
103+
) {
104+
SecondaryButton(
105+
text = "Paste",
106+
onClick = onPasteInvoice,
107+
enabled = !uiState.isLoading,
108+
size = ButtonSize.Small,
109+
modifier = Modifier.weight(1f),
110+
)
111+
}
112+
113+
SectionHeader("AMOUNT OVERRIDE (OPTIONAL)")
114+
SectionFooter("Override the invoice amount for variable-amount invoices")
115+
116+
TextInput(
117+
value = uiState.amountSats,
118+
onValueChange = onAmountChange,
119+
placeholder = "Amount in sats",
120+
singleLine = true,
121+
keyboardOptions = KeyboardOptions(
122+
keyboardType = KeyboardType.Number,
123+
imeAction = ImeAction.Done,
124+
),
125+
modifier = Modifier
126+
.fillMaxWidth()
127+
.padding(vertical = 8.dp),
128+
)
129+
130+
VerticalSpacer(16.dp)
131+
132+
PrimaryButton(
133+
text = "Send Probe",
134+
onClick = onSendProbe,
135+
enabled = !uiState.isLoading && uiState.invoice.isNotBlank(),
136+
isLoading = uiState.isLoading,
137+
modifier = Modifier.fillMaxWidth(),
138+
)
139+
140+
AnimatedVisibility(
141+
visible = uiState.probeResult != null,
142+
enter = fadeIn() + expandVertically(),
143+
exit = fadeOut() + shrinkVertically(),
144+
) {
145+
uiState.probeResult?.let { result ->
146+
Column {
147+
VerticalSpacer(16.dp)
148+
SectionHeader("PROBE RESULTS")
149+
SettingsTextButtonRow(
150+
title = "Status",
151+
iconRes = if (result.success) R.drawable.ic_check else R.drawable.ic_x,
152+
iconTint = if (result.success) Colors.Green else Colors.Red,
153+
value = if (result.success) "Success" else "Failed",
154+
)
155+
SettingsTextButtonRow(
156+
title = "Duration",
157+
iconRes = R.drawable.ic_clock,
158+
value = "${result.durationMs} ms",
159+
)
160+
result.estimatedFeeSats?.let { fee ->
161+
SettingsTextButtonRow(
162+
title = "Estimated Fee",
163+
iconRes = R.drawable.ic_coins,
164+
value = "$fee sats",
165+
)
166+
}
167+
result.errorMessage?.let { error ->
168+
SectionFooter(error)
169+
}
170+
}
171+
}
172+
}
173+
174+
VerticalSpacer(32.dp)
175+
}
176+
}
177+
}
178+
179+
@Preview(showSystemUi = true)
180+
@Composable
181+
private fun Preview() {
182+
var uiState by remember {
183+
mutableStateOf(
184+
ProbingToolUiState(
185+
invoice = "lnbc1000n1pj...",
186+
amountSats = "1000",
187+
probeResult = ProbeResult(
188+
success = true,
189+
durationMs = 342,
190+
estimatedFeeSats = 5uL,
191+
),
192+
)
193+
)
194+
}
195+
196+
AppThemeSurface {
197+
ProbingToolContent(
198+
uiState = uiState,
199+
onBackClick = {},
200+
onInvoiceChange = { uiState = uiState.copy(invoice = it) },
201+
onAmountChange = { uiState = uiState.copy(amountSats = it) },
202+
onPasteInvoice = {},
203+
onSendProbe = {},
204+
)
205+
}
206+
}
207+
208+
@Preview(showSystemUi = true)
209+
@Composable
210+
private fun PreviewFailed() {
211+
var uiState by remember {
212+
mutableStateOf(
213+
ProbingToolUiState(
214+
invoice = "lnbc1000n1pj...",
215+
probeResult = ProbeResult(
216+
success = false,
217+
durationMs = 1250,
218+
errorMessage = "No route found to destination",
219+
),
220+
)
221+
)
222+
}
223+
224+
AppThemeSurface {
225+
ProbingToolContent(
226+
uiState = uiState,
227+
onBackClick = {},
228+
onInvoiceChange = { uiState = uiState.copy(invoice = it) },
229+
onAmountChange = { uiState = uiState.copy(amountSats = it) },
230+
onPasteInvoice = {},
231+
onSendProbe = {},
232+
)
233+
}
234+
}

0 commit comments

Comments
 (0)