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
84 changes: 83 additions & 1 deletion app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -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
}
}
Expand All @@ -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<Args>())
}

@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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: try to use args.amountMsat?.let { msatCeilOf(it) } instead of args.amountMsat?.div(1_000u) if it's still satisfying your requirements

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caution: This should work, but make sure the app is open and a wallet with LN funds is loaded, otherwise the node will not start and the OP will timeout.

.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
Expand All @@ -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<String>,
) : 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<String> = emptyList(),
) : DevResult {
companion object {
fun from(error: Throwable, paymentIds: Set<String> = 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<String>): 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 <reified T> String?.deserialize(): T =
Expand Down
13 changes: 13 additions & 0 deletions app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: verifyBlocking shouldn't be needed, we're already in a suspend block, that's why we wrap tests in test { … }.

Unfortunately Codex always defaults to prefer it, it's smart but not applicable here.

}

@Test
fun `sendProbeForNode delegates to keysend probe and returns payment IDs`() = test {
startNodeForTesting()
Expand Down
Loading