Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ google-services.json
!debug.keystore
keystore.*
!keystore.properties.template
.vscode
102 changes: 91 additions & 11 deletions app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.Process
import android.os.SystemClock
import androidx.core.os.bundleOf
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -114,16 +115,19 @@ private sealed interface DevCommand {
context = TAG,
)

val startedAt = SystemClock.elapsedRealtime()
return deps.lightningRepo().sendProbeForInvoice(args.bolt11, amountSats)
.fold(
onSuccess = {
deps.lightningRepo().waitForProbeOutcome(it.paymentIds, timeout)
.fold(
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds) },
onFailure = { error -> DevResult.ProbeFailure.from(error, it.paymentIds) },
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds, startedAt.durationMs()) },
onFailure = { error ->
DevResult.ProbeFailure.from(error, it.paymentIds, startedAt.durationMs())
},
)
},
onFailure = { DevResult.ProbeFailure.from(it) },
onFailure = { DevResult.ProbeFailure.from(it, durationMs = startedAt.durationMs()) },
)
}
}
Expand All @@ -149,20 +153,24 @@ private sealed interface DevCommand {
val timeout = args.timeoutSeconds.coerceAtLeast(1).seconds

Logger.info(
"Sending keysend probe for target '${args.targetName ?: "unknown"}' nodeId='${args.nodeId}' amountSats='$amountSats'",
"Sending keysend probe for target '${args.targetName ?: "unknown"}' " +
"nodeId='${args.nodeId}' amountSats='$amountSats'",
context = TAG,
)

val startedAt = SystemClock.elapsedRealtime()
return deps.lightningRepo().sendProbeForNode(args.nodeId, amountSats)
.fold(
onSuccess = {
deps.lightningRepo().waitForProbeOutcome(it.paymentIds, timeout)
.fold(
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds) },
onFailure = { error -> DevResult.ProbeFailure.from(error, it.paymentIds) },
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds, startedAt.durationMs()) },
onFailure = { error ->
DevResult.ProbeFailure.from(error, it.paymentIds, startedAt.durationMs())
},
)
},
onFailure = { DevResult.ProbeFailure.from(it) },
onFailure = { DevResult.ProbeFailure.from(it, durationMs = startedAt.durationMs()) },
)
}
}
Expand Down Expand Up @@ -206,6 +214,8 @@ private sealed interface DevResult {
val success: Boolean = true,
val paymentId: String,
val paymentHash: String,
val durationMs: Long,
val routeFeeMsat: ULong?,
val paymentIds: List<String>,
) : DevResult

Expand All @@ -215,12 +225,19 @@ private sealed interface DevResult {
val message: String? = null,
val paymentId: String? = null,
val paymentHash: String? = null,
val shortChannelId: ULong? = null,
val shortChannelId: String? = null,
val durationMs: Long,
val routeFeeMsat: ULong? = null,
val paymentIds: List<String> = emptyList(),
) : DevResult {
companion object {
fun from(error: Throwable, paymentIds: Set<String> = emptySet()) = ProbeFailure(
fun from(
error: Throwable,
paymentIds: Set<String> = emptySet(),
durationMs: Long,
) = ProbeFailure(
message = error.message,
durationMs = durationMs,
paymentIds = paymentIds.toList(),
)
}
Expand All @@ -243,6 +260,7 @@ private sealed interface DevResult {
val graphChannelCount: Int? = null,
val latestRgsSyncTimestamp: ULong? = null,
val latestPathfindingScoresSyncTimestamp: ULong? = null,
val probeRuntimeConfig: ProbeRuntimeConfig,
) : DevResult {
companion object {
fun from(readiness: NodeProbeReadiness) = ProbeReadiness(
Expand All @@ -261,29 +279,91 @@ private sealed interface DevResult {
graphChannelCount = readiness.graphChannelCount,
latestRgsSyncTimestamp = readiness.latestRgsSyncTimestamp,
latestPathfindingScoresSyncTimestamp = readiness.latestPathfindingScoresSyncTimestamp,
probeRuntimeConfig = ProbeRuntimeConfig.from(readiness.probeRuntimeConfig),
)
}
}

@Serializable
data class ProbeRuntimeConfig(
val sampleAmountMsat: ULong,
val route: ProbeRouteConfig,
val scoring: ProbeScoringConfig,
) {
companion object {
fun from(config: to.bitkit.services.ProbeRuntimeConfig) = ProbeRuntimeConfig(
sampleAmountMsat = config.sampleAmountMsat,
route = ProbeRouteConfig(
maxTotalRoutingFeeMsat = config.route.maxTotalRoutingFeeMsat,
maxTotalCltvExpiryDelta = config.route.maxTotalCltvExpiryDelta,
maxPathCount = config.route.maxPathCount,
maxChannelSaturationPowerOfHalf = config.route.maxChannelSaturationPowerOfHalf,
),
scoring = ProbeScoringConfig(
basePenaltyMsat = config.scoring.basePenaltyMsat,
basePenaltyAmountMultiplierMsat = config.scoring.basePenaltyAmountMultiplierMsat,
liquidityPenaltyMultiplierMsat = config.scoring.liquidityPenaltyMultiplierMsat,
liquidityPenaltyAmountMultiplierMsat = config.scoring.liquidityPenaltyAmountMultiplierMsat,
historicalLiquidityPenaltyMultiplierMsat =
config.scoring.historicalLiquidityPenaltyMultiplierMsat,
historicalLiquidityPenaltyAmountMultiplierMsat =
config.scoring.historicalLiquidityPenaltyAmountMultiplierMsat,
antiProbingPenaltyMsat = config.scoring.antiProbingPenaltyMsat,
consideredImpossiblePenaltyMsat = config.scoring.consideredImpossiblePenaltyMsat,
linearSuccessProbability = config.scoring.linearSuccessProbability,
probingDiversityPenaltyMsat = config.scoring.probingDiversityPenaltyMsat,
),
)
}
}

@Serializable
data class ProbeRouteConfig(
val maxTotalRoutingFeeMsat: ULong?,
val maxTotalCltvExpiryDelta: UInt,
val maxPathCount: Int,
val maxChannelSaturationPowerOfHalf: Int,
)

@Serializable
data class ProbeScoringConfig(
val basePenaltyMsat: ULong,
val basePenaltyAmountMultiplierMsat: ULong,
val liquidityPenaltyMultiplierMsat: ULong,
val liquidityPenaltyAmountMultiplierMsat: ULong,
val historicalLiquidityPenaltyMultiplierMsat: ULong,
val historicalLiquidityPenaltyAmountMultiplierMsat: ULong,
val antiProbingPenaltyMsat: ULong,
val consideredImpossiblePenaltyMsat: ULong,
val linearSuccessProbability: Boolean,
val probingDiversityPenaltyMsat: ULong,
)

@Serializable data class Error(val message: String? = null) : DevResult

fun toBundle() = bundleOf(KEY_RESULT to DEV_JSON.encodeToString(this))
}

private fun ProbeOutcome.toDevResult(paymentIds: Set<String>): DevResult = when (this) {
private fun ProbeOutcome.toDevResult(paymentIds: Set<String>, durationMs: Long): DevResult = when (this) {
is ProbeOutcome.Success -> DevResult.ProbeSuccess(
paymentId = paymentId,
paymentHash = paymentHash,
durationMs = durationMs,
routeFeeMsat = routeFeeMsat,
paymentIds = paymentIds.toList(),
)
is ProbeOutcome.Failure -> DevResult.ProbeFailure(
message = "Probe failed",
paymentId = paymentId,
paymentHash = paymentHash,
shortChannelId = shortChannelId,
shortChannelId = shortChannelId?.toString(),
durationMs = durationMs,
routeFeeMsat = routeFeeMsat,
paymentIds = paymentIds.toList(),
)
}

private inline fun <reified T> String?.deserialize(): T =
if (isNullOrBlank()) Json.decodeFromString("{}") else Json.decodeFromString(this)

private fun Long.durationMs() = SystemClock.elapsedRealtime() - this
14 changes: 12 additions & 2 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import to.bitkit.services.LnurlService
import to.bitkit.services.LnurlWithdrawResponse
import to.bitkit.services.LspNotificationsService
import to.bitkit.services.NodeEventHandler
import to.bitkit.services.ProbeRuntimeConfig
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
Expand Down Expand Up @@ -571,8 +572,13 @@ class LightningRepo @Inject constructor(

private suspend fun recordProbeOutcome(event: Event) {
val outcome = when (event) {
is Event.ProbeSuccessful -> ProbeOutcome.Success(event.paymentId, event.paymentHash)
is Event.ProbeFailed -> ProbeOutcome.Failure(event.paymentId, event.paymentHash, event.shortChannelId)
is Event.ProbeSuccessful -> ProbeOutcome.Success(event.paymentId, event.paymentHash, event.routeFeeMsat)
is Event.ProbeFailed -> ProbeOutcome.Failure(
event.paymentId,
event.paymentHash,
event.shortChannelId,
event.routeFeeMsat,
)
else -> return
}

Expand Down Expand Up @@ -1637,6 +1643,7 @@ class LightningRepo @Inject constructor(
graphChannelCount = graph?.channelCount,
latestRgsSyncTimestamp = graph?.latestRgsSyncTimestamp,
latestPathfindingScoresSyncTimestamp = state.nodeStatus?.latestPathfindingScoresSyncTimestamp,
probeRuntimeConfig = lightningService.probeRuntimeConfig(),
syncHealthy = state.isSyncHealthy,
)
}
Expand Down Expand Up @@ -1761,6 +1768,7 @@ data class ProbeReadiness(
val graphChannelCount: Int?,
val latestRgsSyncTimestamp: ULong?,
val latestPathfindingScoresSyncTimestamp: ULong?,
val probeRuntimeConfig: ProbeRuntimeConfig,
val syncHealthy: Boolean,
) {
val ready: Boolean
Expand All @@ -1779,11 +1787,13 @@ sealed interface ProbeOutcome {
data class Success(
override val paymentId: PaymentId,
override val paymentHash: PaymentHash,
val routeFeeMsat: ULong?,
) : ProbeOutcome

data class Failure(
override val paymentId: PaymentId,
override val paymentHash: PaymentHash,
val shortChannelId: ULong?,
val routeFeeMsat: ULong?,
) : ProbeOutcome
}
Loading
Loading