Skip to content

Commit

Permalink
Implement the findroutebetweennodes command
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohit Kumar committed Aug 18, 2023
1 parent c860eaa commit 6f6807c
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 2 deletions.
8 changes: 6 additions & 2 deletions contrib/eclair-cli_autocomplete.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ _eclair_cli() {
# `_init_completion` is a helper function provided by the Bash-completion package.
_init_completion || return

local commands="getinfo connect disconnect open rbfopen cpfpbumpfees close forceclose updaterelayfee peers nodes node allchannels allupdates createinvoice deleteinvoice parseinvoice payinvoice sendtonode sendtoroute getsentinfo getreceivedinfo listreceivedpayments getinvoice listinvoices listpendinginvoices findroute findroutetonode"
local commands="getinfo connect disconnect open rbfopen cpfpbumpfees close forceclose updaterelayfee peers nodes node allchannels allupdates createinvoice deleteinvoice parseinvoice payinvoice sendtonode sendtoroute getsentinfo getreceivedinfo listreceivedpayments getinvoice listinvoices listpendinginvoices findroute findroutetonode findroutebetweennodes"
local common_opts="-p --host"
local connect_opts="--uri --nodeId --address --port"
local disconnect_opts="--nodeId"
Expand All @@ -46,6 +46,7 @@ _eclair_cli() {
local listpendinginvoices_opts="--from --to --count --skip"
local findroute_opts="--invoice --amountMsat --ignoreNodeIds --ignoreShortChannelIds --format --maxFeeMsat --includeLocalChannelCost --pathFindingExperimentName"
local findroutetonode_opts="--nodeId --amountMsat --ignoreNodeIds --ignoreShortChannelIds --format --maxFeeMsat --includeLocalChannelCost --pathFindingExperimentName"
local findroutebetweennodes_opts="--sourceNodeId --targetNodeId --amountMsat --ignoreNodeIds --ignoreShortChannelIds --format --maxFeeMsat --includeLocalChannelCost --pathFindingExperimentName"
# If the current word starts with a dash (-), it's an option rather than a command
if [[ ${cur} == -* ]]; then
local cmd=""
Expand Down Expand Up @@ -132,7 +133,10 @@ _eclair_cli() {
COMPREPLY=( $(compgen -W "${findroute_opts} ${common_opts}" -- ${cur}) )
;;
findroutetonode)
COMPREPLY=( $(compgen -W "${findroutetonode_opts} ${common_opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${findroutetonode_opts} ${common_opts}" -- ${cur}) )
;;
findroutebetweennodes)
COMPREPLY=( $(compgen -W "${findroutebetweennodes_opts} ${common_opts}" -- ${cur}) )
;;
*)
COMPREPLY=( $(compgen -W "${common_opts}" -- ${cur}) )
Expand Down
1 change: 1 addition & 0 deletions src/nativeMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fun main(args: Array<String>) {
ListPendingInvoicesCommand(resultWriter, apiClientBuilder),
FindRouteCommand(resultWriter, apiClientBuilder),
FindRouteToNodeCommand(resultWriter, apiClientBuilder),
FindRouteBetweenNodesCommand(resultWriter, apiClientBuilder),
)
parser.parse(args)
}
47 changes: 47 additions & 0 deletions src/nativeMain/kotlin/api/EclairClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ interface IEclairClient {
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String>

suspend fun findroutebetweennodes(
sourceNodeId: String,
targetNodeId: String,
amountMsat: Int,
ignoreNodeIds: List<String>?,
ignoreShortChannelIds: List<String>?,
format: String?,
maxFeeMsat: Int?,
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String>
}

class EclairClient(private val apiHost: String, private val apiPassword: String) : IEclairClient {
Expand Down Expand Up @@ -837,4 +849,39 @@ class EclairClient(private val apiHost: String, private val apiPassword: String)
Either.Left(ApiError(0, e.message ?: "Unknown exception"))
}
}

override suspend fun findroutebetweennodes(
sourceNodeId: String,
targetNodeId: String,
amountMsat: Int,
ignoreNodeIds: List<String>?,
ignoreShortChannelIds: List<String>?,
format: String?,
maxFeeMsat: Int?,
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String> {
return try {
val response: HttpResponse = httpClient.submitForm(
url = "$apiHost/findroutebetweennodes",
formParameters = Parameters.build {
append("sourceNodeId", sourceNodeId)
append("targetNodeId", targetNodeId)
append("amountMsat", amountMsat.toString())
ignoreNodeIds?.let { append("ignoreNodeIds", it.joinToString(",")) }
ignoreShortChannelIds?.let { append("ignoreShortChannelIds", it.joinToString(",")) }
format?.let { append("format", it) }
maxFeeMsat?.let { append("maxFeeMsat", it.toString()) }
includeLocalChannelCost?.let { append("includeLocalChannelCost", it.toString()) }
pathFindingExperimentName?.let { append("pathFindingExperimentName", it) }
}
)
when (response.status) {
HttpStatusCode.OK -> Either.Right((response.bodyAsText()))
else -> Either.Left(convertHttpError(response.status))
}
} catch (e: Throwable) {
Either.Left(ApiError(0, e.message ?: "Unknown exception"))
}
}
}
71 changes: 71 additions & 0 deletions src/nativeMain/kotlin/commands/FindRouteBetweenNodes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package commands

import IResultWriter
import api.IEclairClientBuilder
import arrow.core.flatMap
import kotlinx.cli.ArgType
import kotlinx.coroutines.runBlocking
import types.FindRouteResponse
import types.Serialization

class FindRouteBetweenNodesCommand(
private val resultWriter: IResultWriter,
private val eclairClientBuilder: IEclairClientBuilder
) : BaseCommand(
"findroutebetweennodes",
"Finds a route between two nodes."
) {
private val sourceNodeId by option(
ArgType.String,
description = "The destination of the route"
)
private val targetNodeId by option(
ArgType.String,
description = "The destination of the route"
)
private val amountMsat by option(
ArgType.Int,
description = "The amount that should go through the route"
)
private val ignoreNodeIds by option(
ArgType.String,
description = "A list of nodes to exclude from path-finding"
)
private val ignoreShortChannelIds by option(
ArgType.String,
description = "A list of channels to exclude from path-finding"
)
private val format by option(
ArgType.String,
description = "Format that will be used for the resulting route"
)
private val maxFeeMsat by option(
ArgType.Int,
description = "Maximum fee allowed for this payment"
)
private val includeLocalChannelCost by option(
ArgType.Boolean,
description = "If true, the relay fees of local channels will be counted"
)
private val pathFindingExperimentName by option(
ArgType.String,
description = "Name of the path-finding configuration that should be used"
)

override fun execute() = runBlocking {
val eclairClient = eclairClientBuilder.build(host, password)
val result = eclairClient.findroutebetweennodes(
sourceNodeId!!,
targetNodeId!!,
amountMsat!!,
ignoreNodeIds?.split(","),
ignoreShortChannelIds?.split(","),
format,
maxFeeMsat,
includeLocalChannelCost,
pathFindingExperimentName
).flatMap { apiResponse -> Serialization.decode<FindRouteResponse>(apiResponse) }
.map { decoded -> Serialization.encode(decoded) }
resultWriter.write(result)
}
}
68 changes: 68 additions & 0 deletions src/nativeTest/kotlin/commands/FindRouteBetweenNodesTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package commands

import api.IEclairClientBuilder
import kotlinx.cli.ArgParser
import kotlinx.cli.ExperimentalCli
import kotlinx.serialization.json.Json
import mocks.DummyEclairClient
import mocks.DummyResultWriter
import mocks.FailingEclairClient
import types.ApiError
import types.FindRouteResponse
import kotlin.test.*

@OptIn(ExperimentalCli::class)
class FindRouteBetweenNodesCommandTest {
private fun runTest(eclairClient: IEclairClientBuilder): DummyResultWriter {
val resultWriter = DummyResultWriter()
val command = FindRouteBetweenNodesCommand(resultWriter, eclairClient)
val parser = ArgParser("test")
parser.subcommands(command)
parser.parse(
arrayOf(
"findroutebetweennodes",
"-p",
"password",
"--sourceNodeId",
"03c5b161c16e9f8ef3f3bccfb74a6e9a3b423dd41fe2848174b7209f1c2ea25dad",
"--targetNodeId",
"02f666711319435b7905dd77d10c269d8d50c02668b975f526577167d370b50a3e",
"--amountMsat",
"1000"
)
)
return resultWriter
}

@Test
fun `successful request`() {
val resultWriter =
runTest(DummyEclairClient(findroutebetweennodesResponse = DummyEclairClient.validFindRouteBetweenNodesResponse))
assertNull(resultWriter.lastError)
assertNotNull(resultWriter.lastResult)
val format = Json { ignoreUnknownKeys = true }
assertEquals(
format.decodeFromString(
FindRouteResponse.serializer(),
DummyEclairClient.validFindRouteBetweenNodesResponse
),
format.decodeFromString(FindRouteResponse.serializer(), resultWriter.lastResult!!),
)
}

@Test
fun `api error`() {
val error = ApiError(42, "test failure message")
val resultWriter = runTest(FailingEclairClient(error))
assertNull(resultWriter.lastResult)
assertEquals(error, resultWriter.lastError)
}

@Test
fun `serialization error`() {
val resultWriter = runTest(DummyEclairClient(findroutebetweennodesResponse = "{invalidJson}"))
assertNull(resultWriter.lastResult)
assertNotNull(resultWriter.lastError)
assertTrue(resultWriter.lastError!!.message.contains("api response could not be parsed"))
}
}
37 changes: 37 additions & 0 deletions src/nativeTest/kotlin/mocks/EclairClientMocks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class DummyEclairClient(
private val listpendinginvoicesResponse: String = validListPendingInvoicesResponse,
private val findrouteResponse: String = validFindRouteResponse,
private val findroutetonodeResponse: String = validFindRouteToNodeResponse,
private val findroutebetweennodesResponse: String = validFindRouteBetweenNodesResponse,
) : IEclairClient, IEclairClientBuilder {
override fun build(apiHost: String, apiPassword: String): IEclairClient = this
override suspend fun getInfo(): Either<ApiError, String> = Either.Right(getInfoResponse)
Expand Down Expand Up @@ -193,6 +194,18 @@ class DummyEclairClient(
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Right(findroutetonodeResponse)

override suspend fun findroutebetweennodes(
sourceNodeId: String,
targetNodeId: String,
amountMsat: Int,
ignoreNodeIds: List<String>?,
ignoreShortChannelIds: List<String>?,
format: String?,
maxFeeMsat: Int?,
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Right(findroutebetweennodesResponse)

companion object {
val validGetInfoResponse =
"""{"version":"0.9.0","nodeId":"03e319aa4ecc7a89fb8b3feb6efe9075864b91048bff5bef14efd55a69760ddf17","alias":"alice","color":"#49daaa","features":{"activated":{"var_onion_optin":"mandatory","option_static_remotekey":"optional"},"unknown":[151,178]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":107,"publicAddresses":[],"instanceId":"be74bd9a-fc54-4f24-bc41-0477c9ce2fb4"}"""
Expand Down Expand Up @@ -902,6 +915,18 @@ class DummyEclairClient(
]
}
]
}"""
val validFindRouteBetweenNodesResponse = """{
"routes": [
{
"amount": 5000,
"nodeIds": [
"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96",
"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
]
}
]
}"""
}
}
Expand Down Expand Up @@ -1045,4 +1070,16 @@ class FailingEclairClient(private val error: ApiError) : IEclairClient, IEclairC
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Left(error)

override suspend fun findroutebetweennodes(
sourceNodeId: String,
targetNodeId: String,
amountMsat: Int,
ignoreNodeIds: List<String>?,
ignoreShortChannelIds: List<String>?,
format: String?,
maxFeeMsat: Int?,
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Left(error)
}

0 comments on commit 6f6807c

Please sign in to comment.