diff --git a/app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt b/app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt index a4d893d65..4effe5a45 100644 --- a/app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt +++ b/app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt @@ -15,9 +15,12 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import to.bitkit.async.ServiceQueue import to.bitkit.repositories.LightningRepo +import to.bitkit.repositories.ProbeOutcome import to.bitkit.utils.Logger +import kotlin.time.Duration.Companion.seconds private const val TAG = "DevToolsProvider" +private val DEV_JSON = Json { encodeDefaults = true } class DevToolsProvider : ContentProvider() { @@ -56,6 +59,7 @@ private sealed interface DevCommand { companion object { fun parse(method: String, arg: String?): DevCommand? = when (method) { CreateInvoice.METHOD -> CreateInvoice.parse(arg) + ProbeInvoice.METHOD -> ProbeInvoice.parse(arg) else -> null } } @@ -80,6 +84,44 @@ private sealed interface DevCommand { }, ) } + + data class ProbeInvoice(val args: Args) : DevCommand { + companion object { + const val METHOD = "probeInvoice" + fun parse(arg: String?) = ProbeInvoice(arg.deserialize()) + } + + @Serializable + data class Args( + val targetName: String? = null, + val bolt11: String, + val amountMsat: ULong? = null, + val amountSats: ULong? = null, + val timeoutSeconds: Long = 90, + ) + + override suspend fun execute(deps: DevToolsProvider.Dependencies): DevResult { + val amountSats = args.amountSats ?: args.amountMsat?.div(1_000u) + val timeout = args.timeoutSeconds.coerceAtLeast(1).seconds + + Logger.info( + "Sending probe for target '${args.targetName ?: "unknown"}' amountSats='${amountSats ?: "invoice"}'", + context = TAG, + ) + + 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) }, + ) + }, + onFailure = { DevResult.ProbeFailure.from(it) }, + ) + } + } } @Serializable @@ -91,9 +133,49 @@ private sealed interface DevResult { @Serializable data class Invoice(val bolt11: String) : DevResult + @Serializable + data class ProbeSuccess( + val success: Boolean = true, + val paymentId: String, + val paymentHash: String, + val paymentIds: List, + ) : DevResult + + @Serializable + data class ProbeFailure( + val success: Boolean = false, + val message: String? = null, + val paymentId: String? = null, + val paymentHash: String? = null, + val shortChannelId: ULong? = null, + val paymentIds: List = emptyList(), + ) : DevResult { + companion object { + fun from(error: Throwable, paymentIds: Set = emptySet()) = ProbeFailure( + message = error.message, + paymentIds = paymentIds.toList(), + ) + } + } + @Serializable data class Error(val message: String? = null) : DevResult - fun toBundle() = bundleOf(KEY_RESULT to Json.encodeToString(this)) + fun toBundle() = bundleOf(KEY_RESULT to DEV_JSON.encodeToString(this)) +} + +private fun ProbeOutcome.toDevResult(paymentIds: Set): DevResult = when (this) { + is ProbeOutcome.Success -> DevResult.ProbeSuccess( + paymentId = paymentId, + paymentHash = paymentHash, + paymentIds = paymentIds.toList(), + ) + is ProbeOutcome.Failure -> DevResult.ProbeFailure( + message = "Probe failed", + paymentId = paymentId, + paymentHash = paymentHash, + shortChannelId = shortChannelId, + paymentIds = paymentIds.toList(), + ) } private inline fun String?.deserialize(): T = diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 877e23dd7..a2ff59b32 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -1267,6 +1267,19 @@ class LightningRepoTest : BaseUnitTest() { assertEquals(setOf(probePaymentA, probePaymentB), result.getOrThrow().paymentIds) } + @Test + fun `sendProbeForInvoice delegates amount probes when sats are provided`() = test { + startNodeForTesting() + whenever(lightningService.sendProbesUsingAmount("lnbc1", 42_000uL)) + .thenReturn(Result.success(setOf(probePaymentA))) + + val result = sut.sendProbeForInvoice("lnbc1", amountSats = 42uL) + + assertTrue(result.isSuccess) + assertEquals(setOf(probePaymentA), result.getOrThrow().paymentIds) + verifyBlocking(lightningService) { sendProbesUsingAmount("lnbc1", 42_000uL) } + } + @Test fun `sendProbeForNode delegates to keysend probe and returns payment IDs`() = test { startNodeForTesting()