Skip to content

Commit eeee577

Browse files
authored
Merge pull request #781 from synonymdev/feat/remote-scorer
feat: use external scores from blocktank
2 parents cf22194 + 5849cd5 commit eeee577

File tree

6 files changed

+138
-21
lines changed

6 files changed

+138
-21
lines changed

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ internal object Env {
6767
else -> null
6868
}
6969

70+
val ldkScorerUrl
71+
get() = when (network) {
72+
Network.BITCOIN -> "https://api.blocktank.to/scorer.bin"
73+
Network.REGTEST -> "https://api.stag0.blocktank.to/scorer"
74+
else -> null
75+
}
76+
7077
val vssStoreIdPrefix get() = "bitkit_v1_${network.name.lowercase()}"
7178

7279
val vssServerUrl

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ 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
3635
import org.lightningdevkit.ldknode.ChannelConfig
3736
import org.lightningdevkit.ldknode.ChannelDataMigration
3837
import org.lightningdevkit.ldknode.ChannelDetails
@@ -1099,13 +1098,16 @@ class LightningRepo @Inject constructor(
10991098
// region probing
11001099
suspend fun sendProbeForInvoice(bolt11: String, amountSats: ULong? = null): Result<Unit> =
11011100
executeWhenNodeRunning("sendProbeForInvoice") {
1101+
Logger.debug(
1102+
"sendProbeForInvoice: amountSats=${amountSats ?: "null (using invoice amount)"}",
1103+
context = TAG
1104+
)
11021105
runCatching {
1103-
val invoice = Bolt11Invoice.fromStr(bolt11)
11041106
if (amountSats != null) {
11051107
val amountMsat = amountSats * 1000u
1106-
lightningService.sendProbesUsingAmount(invoice, amountMsat)
1108+
lightningService.sendProbesUsingAmount(bolt11, amountMsat)
11071109
} else {
1108-
lightningService.sendProbes(invoice)
1110+
lightningService.sendProbes(bolt11)
11091111
}
11101112
}.getOrElse {
11111113
Result.failure(it)

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

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class LightningService @Inject constructor(
138138
setCustomLogger(LdkLogWriter())
139139
configureChainSource(customServerUrl)
140140
configureGossipSource(customRgsServerUrl)
141+
configureScorerSource()
141142

142143
if (channelMigration != null) {
143144
setChannelDataMigration(channelMigration)
@@ -186,6 +187,12 @@ class LightningService @Inject constructor(
186187
}
187188
}
188189

190+
private fun Builder.configureScorerSource() {
191+
val scorerUrl = Env.ldkScorerUrl ?: return
192+
Logger.info("Using pathfinding scores source: '$scorerUrl'", context = TAG)
193+
setPathfindingScoresSource(scorerUrl)
194+
}
195+
189196
private suspend fun Builder.configureChainSource(customServerUrl: String? = null) {
190197
val serverUrl = customServerUrl ?: settingsStore.data.first().electrumServer
191198
Logger.info("Using onchain source Electrum Sever url: $serverUrl", context = TAG)
@@ -652,27 +659,48 @@ class LightningService @Inject constructor(
652659
// endregion
653660

654661
// region probing
655-
suspend fun sendProbes(invoice: Bolt11Invoice): Result<Unit> {
662+
suspend fun sendProbes(bolt11: String): Result<Unit> {
656663
val node = this.node ?: throw ServiceError.NodeNotSetup()
657664

665+
val bolt11Invoice = runCatching { Bolt11Invoice.fromStr(bolt11) }
666+
.getOrElse { throw LdkError(it as NodeException) }
667+
668+
val invoiceAmountMsat = bolt11Invoice.amountMilliSatoshis()
669+
Logger.debug(
670+
"sendProbes: invoiceAmountMsat=$invoiceAmountMsat (${invoiceAmountMsat?.let { it / 1000u }} sats)",
671+
context = TAG
672+
)
673+
658674
return ServiceQueue.LDK.background {
659675
runCatching {
660-
node.bolt11Payment().sendProbes(invoice, null)
676+
node.bolt11Payment().sendProbes(bolt11Invoice, null)
661677
Result.success(Unit)
662678
}.getOrElse {
679+
dumpNetworkGraphInfo(bolt11)
663680
Result.failure(if (it is NodeException) LdkError(it) else it)
664681
}
665682
}
666683
}
667684

668-
suspend fun sendProbesUsingAmount(invoice: Bolt11Invoice, amountMsat: ULong): Result<Unit> {
685+
suspend fun sendProbesUsingAmount(bolt11: String, amountMsat: ULong): Result<Unit> {
669686
val node = this.node ?: throw ServiceError.NodeNotSetup()
670687

688+
val bolt11Invoice = runCatching { Bolt11Invoice.fromStr(bolt11) }
689+
.getOrElse { throw LdkError(it as NodeException) }
690+
691+
val invoiceAmountMsat = bolt11Invoice.amountMilliSatoshis()
692+
Logger.debug(
693+
"sendProbesUsingAmount: customAmountMsat=$amountMsat (${amountMsat / 1000u} sats), " +
694+
"invoiceAmountMsat=$invoiceAmountMsat (${invoiceAmountMsat?.let { it / 1000u }} sats)",
695+
context = TAG
696+
)
697+
671698
return ServiceQueue.LDK.background {
672699
runCatching {
673-
node.bolt11Payment().sendProbesUsingAmount(invoice, amountMsat, null)
700+
node.bolt11Payment().sendProbesUsingAmount(bolt11Invoice, amountMsat, null)
674701
Result.success(Unit)
675702
}.getOrElse {
703+
dumpNetworkGraphInfo(bolt11)
676704
Result.failure(if (it is NodeException) LdkError(it) else it)
677705
}
678706
}
@@ -970,6 +998,28 @@ class LightningService @Inject constructor(
970998
sb.appendLine(" Total nodes: ${allNodes.size}")
971999
sb.appendLine(" Total channels: ${allChannels.size}")
9721000

1001+
// Payee and route hints check
1002+
runCatching {
1003+
val invoice = Bolt11Invoice.fromStr(bolt11)
1004+
val payeeNodeId = invoice.recoverPayeePubKey()
1005+
sb.appendLine("\nInvoice Payee & Route Hints:")
1006+
sb.appendLine(" - Amount: ${invoice.amountMilliSatoshis()} msat")
1007+
sb.appendLine(" - Payee Node ID: $payeeNodeId")
1008+
sb.appendLine(" - Payee in graph: ${allNodes.any { it == payeeNodeId }}")
1009+
1010+
val routeHints = invoice.routeHints()
1011+
sb.appendLine(" - Route hints: ${routeHints.size} group(s)")
1012+
routeHints.forEachIndexed { groupIdx, hops ->
1013+
hops.forEachIndexed { hopIdx, hop ->
1014+
val hopInGraph = allNodes.any { it == hop.srcNodeId }
1015+
sb.appendLine(
1016+
" Hint[$groupIdx][$hopIdx]: src=${hop.srcNodeId.take(nodeIdPreviewLength)}... " +
1017+
"scid=${hop.shortChannelId} inGraph=$hopInGraph"
1018+
)
1019+
}
1020+
}
1021+
}
1022+
9731023
// Check for trusted peers in graph
9741024
sb.appendLine("\n Checking for trusted peers in network graph:")
9751025
var foundTrustedNodes = 0

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ private fun NavGraphBuilder.settings(
892892
LdkDebugScreen(navController)
893893
}
894894
composableWithDefaultTransitions<Routes.ProbingTool> {
895-
ProbingToolScreen(navController)
895+
ProbingToolScreen(it.savedStateHandle, navController)
896896
}
897897
composableWithDefaultTransitions<Routes.FeeSettings> {
898898
FeeSettingsScreen(navController)

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState
1616
import androidx.compose.foundation.text.KeyboardOptions
1717
import androidx.compose.foundation.verticalScroll
1818
import androidx.compose.runtime.Composable
19+
import androidx.compose.runtime.LaunchedEffect
1920
import androidx.compose.runtime.getValue
2021
import androidx.compose.runtime.mutableStateOf
2122
import androidx.compose.runtime.remember
@@ -26,8 +27,10 @@ import androidx.compose.ui.text.input.KeyboardType
2627
import androidx.compose.ui.tooling.preview.Preview
2728
import androidx.compose.ui.unit.dp
2829
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
30+
import androidx.lifecycle.SavedStateHandle
2931
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3032
import androidx.navigation.NavController
33+
import kotlinx.coroutines.flow.filterNotNull
3134
import to.bitkit.R
3235
import to.bitkit.ui.components.ButtonSize
3336
import to.bitkit.ui.components.PrimaryButton
@@ -37,9 +40,11 @@ import to.bitkit.ui.components.VerticalSpacer
3740
import to.bitkit.ui.components.settings.SectionFooter
3841
import to.bitkit.ui.components.settings.SectionHeader
3942
import to.bitkit.ui.components.settings.SettingsTextButtonRow
43+
import to.bitkit.ui.navigateToScanner
4044
import to.bitkit.ui.scaffold.AppTopBar
41-
import to.bitkit.ui.scaffold.DrawerNavIcon
45+
import to.bitkit.ui.scaffold.ScanNavIcon
4246
import to.bitkit.ui.scaffold.ScreenColumn
47+
import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY
4348
import to.bitkit.ui.theme.AppThemeSurface
4449
import to.bitkit.ui.theme.Colors
4550
import to.bitkit.viewmodels.ProbeResult
@@ -48,14 +53,25 @@ import to.bitkit.viewmodels.ProbingToolViewModel
4853

4954
@Composable
5055
fun ProbingToolScreen(
56+
savedStateHandle: SavedStateHandle,
5157
navController: NavController,
5258
viewModel: ProbingToolViewModel = hiltViewModel(),
5359
) {
5460
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
5561

62+
LaunchedEffect(savedStateHandle) {
63+
savedStateHandle.getStateFlow<String?>(SCAN_RESULT_KEY, null)
64+
.filterNotNull()
65+
.collect { scannedData ->
66+
viewModel.updateInvoice(scannedData)
67+
savedStateHandle.remove<String>(SCAN_RESULT_KEY)
68+
}
69+
}
70+
5671
ProbingToolContent(
5772
uiState = uiState,
5873
onBackClick = { navController.popBackStack() },
74+
onScanClick = { navController.navigateToScanner(isCalledForResult = true) },
5975
onInvoiceChange = viewModel::updateInvoice,
6076
onAmountChange = viewModel::updateAmountSats,
6177
onPasteInvoice = viewModel::pasteInvoice,
@@ -67,6 +83,7 @@ fun ProbingToolScreen(
6783
private fun ProbingToolContent(
6884
uiState: ProbingToolUiState,
6985
onBackClick: () -> Unit,
86+
onScanClick: () -> Unit,
7087
onInvoiceChange: (String) -> Unit,
7188
onAmountChange: (String) -> Unit,
7289
onPasteInvoice: () -> Unit,
@@ -76,7 +93,7 @@ private fun ProbingToolContent(
7693
AppTopBar(
7794
titleText = "Probing Tool",
7895
onBackClick = onBackClick,
79-
actions = { DrawerNavIcon() },
96+
actions = { ScanNavIcon(onScanClick) },
8097
)
8198
Column(
8299
modifier = Modifier
@@ -85,7 +102,7 @@ private fun ProbingToolContent(
85102
.verticalScroll(rememberScrollState())
86103
) {
87104
SectionHeader("PROBE INVOICE", padding = PaddingValues(0.dp))
88-
SectionFooter("Enter a Lightning invoice to probe the payment route")
105+
SectionFooter("Enter a Lightning invoice or LNURL to probe the payment route")
89106

90107
TextInput(
91108
value = uiState.invoice,
@@ -108,10 +125,22 @@ private fun ProbingToolContent(
108125
size = ButtonSize.Small,
109126
modifier = Modifier.weight(1f),
110127
)
128+
SecondaryButton(
129+
text = "Scan",
130+
onClick = onScanClick,
131+
enabled = !uiState.isLoading,
132+
size = ButtonSize.Small,
133+
modifier = Modifier.weight(1f),
134+
)
111135
}
112136

113-
SectionHeader("AMOUNT OVERRIDE (OPTIONAL)")
114-
SectionFooter("Override the invoice amount for variable-amount invoices")
137+
if (uiState.isLnurlPay) {
138+
SectionHeader("AMOUNT (REQUIRED)")
139+
SectionFooter("Enter the amount in sats to probe via LNURL")
140+
} else {
141+
SectionHeader("AMOUNT OVERRIDE (OPTIONAL)")
142+
SectionFooter("Override the invoice amount for variable-amount invoices")
143+
}
115144

116145
TextInput(
117146
value = uiState.amountSats,
@@ -132,7 +161,8 @@ private fun ProbingToolContent(
132161
PrimaryButton(
133162
text = "Send Probe",
134163
onClick = onSendProbe,
135-
enabled = !uiState.isLoading && uiState.invoice.isNotBlank(),
164+
enabled = !uiState.isLoading && uiState.invoice.isNotBlank() &&
165+
(!uiState.isLnurlPay || uiState.amountSats.isNotBlank()),
136166
isLoading = uiState.isLoading,
137167
modifier = Modifier.fillMaxWidth(),
138168
)
@@ -197,6 +227,7 @@ private fun Preview() {
197227
ProbingToolContent(
198228
uiState = uiState,
199229
onBackClick = {},
230+
onScanClick = {},
200231
onInvoiceChange = { uiState = uiState.copy(invoice = it) },
201232
onAmountChange = { uiState = uiState.copy(amountSats = it) },
202233
onPasteInvoice = {},
@@ -225,6 +256,7 @@ private fun PreviewFailed() {
225256
ProbingToolContent(
226257
uiState = uiState,
227258
onBackClick = {},
259+
onScanClick = {},
228260
onInvoiceChange = { uiState = uiState.copy(invoice = it) },
229261
onAmountChange = { uiState = uiState.copy(amountSats = it) },
230262
onPasteInvoice = {},

app/src/main/java/to/bitkit/viewmodels/ProbingToolViewModel.kt

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.asStateFlow
1414
import kotlinx.coroutines.flow.update
1515
import kotlinx.coroutines.launch
1616
import to.bitkit.di.BgDispatcher
17+
import to.bitkit.ext.maxSendableSat
18+
import to.bitkit.ext.minSendableSat
1719
import to.bitkit.models.Toast
1820
import to.bitkit.repositories.LightningRepo
1921
import to.bitkit.services.CoreService
@@ -33,7 +35,8 @@ class ProbingToolViewModel @Inject constructor(
3335
val uiState = _uiState.asStateFlow()
3436

3537
fun updateInvoice(invoice: String) {
36-
_uiState.update { it.copy(invoice = invoice) }
38+
_uiState.update { it.copy(invoice = invoice, probeResult = null) }
39+
detectInputType(invoice)
3740
}
3841

3942
fun updateAmountSats(amount: String) {
@@ -56,7 +59,7 @@ class ProbingToolViewModel @Inject constructor(
5659
return
5760
}
5861

59-
_uiState.update { it.copy(invoice = pastedInvoice) }
62+
updateInvoice(pastedInvoice)
6063
}
6164

6265
fun sendProbe() {
@@ -71,7 +74,8 @@ class ProbingToolViewModel @Inject constructor(
7174
viewModelScope.launch(bgDispatcher) {
7275
_uiState.update { it.copy(isLoading = true, probeResult = null) }
7376

74-
val bolt11 = extractBolt11Invoice(input)
77+
val amountSats = _uiState.value.amountSats.toULongOrNull()
78+
val bolt11 = extractBolt11Invoice(input, amountSats)
7579
if (bolt11 == null) {
7680
ToastEventBus.send(
7781
type = Toast.ToastType.WARNING,
@@ -82,7 +86,6 @@ class ProbingToolViewModel @Inject constructor(
8286
return@launch
8387
}
8488

85-
val amountSats = _uiState.value.amountSats.toULongOrNull()
8689
val startTime = System.currentTimeMillis()
8790

8891
lightningRepo.sendProbeForInvoice(bolt11, amountSats)
@@ -93,14 +96,36 @@ class ProbingToolViewModel @Inject constructor(
9396
}
9497
}
9598

96-
private suspend fun extractBolt11Invoice(input: String): String? = runCatching {
99+
private fun detectInputType(input: String) {
100+
viewModelScope.launch(bgDispatcher) {
101+
val data = runCatching { coreService.decode(input.trim()) }.getOrNull()
102+
if (data is Scanner.LnurlPay) {
103+
val min = data.data.minSendableSat()
104+
val max = data.data.maxSendableSat()
105+
val isFixed = min == max && min > 0uL
106+
_uiState.update {
107+
it.copy(
108+
isLnurlPay = true,
109+
amountSats = if (isFixed) min.toString() else it.amountSats,
110+
)
111+
}
112+
} else {
113+
_uiState.update { it.copy(isLnurlPay = false) }
114+
}
115+
}
116+
}
117+
118+
private suspend fun extractBolt11Invoice(input: String, amountSats: ULong?): String? = runCatching {
97119
when (val decoded = coreService.decode(input)) {
98120
is Scanner.Lightning -> decoded.invoice.bolt11
99121
is Scanner.OnChain -> {
100122
val lightningParam = decoded.invoice.params?.get("lightning") ?: return@runCatching null
101123
(coreService.decode(lightningParam) as? Scanner.Lightning)?.invoice?.bolt11
102124
}
103-
125+
is Scanner.LnurlPay -> {
126+
val amount = amountSats ?: return@runCatching null
127+
lightningRepo.fetchLnurlInvoice(decoded.data.callback, amount).getOrThrow().bolt11
128+
}
104129
else -> null
105130
}
106131
}.getOrNull()
@@ -144,6 +169,7 @@ data class ProbingToolUiState(
144169
val invoice: String = "",
145170
val amountSats: String = "",
146171
val isLoading: Boolean = false,
172+
val isLnurlPay: Boolean = false,
147173
val probeResult: ProbeResult? = null,
148174
)
149175

0 commit comments

Comments
 (0)