Skip to content

Commit 6f6807c

Browse files
author
Mohit Kumar
committed
Implement the findroutebetweennodes command
1 parent c860eaa commit 6f6807c

File tree

6 files changed

+230
-2
lines changed

6 files changed

+230
-2
lines changed

contrib/eclair-cli_autocomplete.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ _eclair_cli() {
1919
# `_init_completion` is a helper function provided by the Bash-completion package.
2020
_init_completion || return
2121

22-
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"
22+
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"
2323
local common_opts="-p --host"
2424
local connect_opts="--uri --nodeId --address --port"
2525
local disconnect_opts="--nodeId"
@@ -46,6 +46,7 @@ _eclair_cli() {
4646
local listpendinginvoices_opts="--from --to --count --skip"
4747
local findroute_opts="--invoice --amountMsat --ignoreNodeIds --ignoreShortChannelIds --format --maxFeeMsat --includeLocalChannelCost --pathFindingExperimentName"
4848
local findroutetonode_opts="--nodeId --amountMsat --ignoreNodeIds --ignoreShortChannelIds --format --maxFeeMsat --includeLocalChannelCost --pathFindingExperimentName"
49+
local findroutebetweennodes_opts="--sourceNodeId --targetNodeId --amountMsat --ignoreNodeIds --ignoreShortChannelIds --format --maxFeeMsat --includeLocalChannelCost --pathFindingExperimentName"
4950
# If the current word starts with a dash (-), it's an option rather than a command
5051
if [[ ${cur} == -* ]]; then
5152
local cmd=""
@@ -132,7 +133,10 @@ _eclair_cli() {
132133
COMPREPLY=( $(compgen -W "${findroute_opts} ${common_opts}" -- ${cur}) )
133134
;;
134135
findroutetonode)
135-
COMPREPLY=( $(compgen -W "${findroutetonode_opts} ${common_opts}" -- ${cur}) )
136+
COMPREPLY=( $(compgen -W "${findroutetonode_opts} ${common_opts}" -- ${cur}) )
137+
;;
138+
findroutebetweennodes)
139+
COMPREPLY=( $(compgen -W "${findroutebetweennodes_opts} ${common_opts}" -- ${cur}) )
136140
;;
137141
*)
138142
COMPREPLY=( $(compgen -W "${common_opts}" -- ${cur}) )

src/nativeMain/kotlin/Main.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fun main(args: Array<String>) {
3737
ListPendingInvoicesCommand(resultWriter, apiClientBuilder),
3838
FindRouteCommand(resultWriter, apiClientBuilder),
3939
FindRouteToNodeCommand(resultWriter, apiClientBuilder),
40+
FindRouteBetweenNodesCommand(resultWriter, apiClientBuilder),
4041
)
4142
parser.parse(args)
4243
}

src/nativeMain/kotlin/api/EclairClient.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,18 @@ interface IEclairClient {
176176
includeLocalChannelCost: Boolean?,
177177
pathFindingExperimentName: String?
178178
): Either<ApiError, String>
179+
180+
suspend fun findroutebetweennodes(
181+
sourceNodeId: String,
182+
targetNodeId: String,
183+
amountMsat: Int,
184+
ignoreNodeIds: List<String>?,
185+
ignoreShortChannelIds: List<String>?,
186+
format: String?,
187+
maxFeeMsat: Int?,
188+
includeLocalChannelCost: Boolean?,
189+
pathFindingExperimentName: String?
190+
): Either<ApiError, String>
179191
}
180192

181193
class EclairClient(private val apiHost: String, private val apiPassword: String) : IEclairClient {
@@ -837,4 +849,39 @@ class EclairClient(private val apiHost: String, private val apiPassword: String)
837849
Either.Left(ApiError(0, e.message ?: "Unknown exception"))
838850
}
839851
}
852+
853+
override suspend fun findroutebetweennodes(
854+
sourceNodeId: String,
855+
targetNodeId: String,
856+
amountMsat: Int,
857+
ignoreNodeIds: List<String>?,
858+
ignoreShortChannelIds: List<String>?,
859+
format: String?,
860+
maxFeeMsat: Int?,
861+
includeLocalChannelCost: Boolean?,
862+
pathFindingExperimentName: String?
863+
): Either<ApiError, String> {
864+
return try {
865+
val response: HttpResponse = httpClient.submitForm(
866+
url = "$apiHost/findroutebetweennodes",
867+
formParameters = Parameters.build {
868+
append("sourceNodeId", sourceNodeId)
869+
append("targetNodeId", targetNodeId)
870+
append("amountMsat", amountMsat.toString())
871+
ignoreNodeIds?.let { append("ignoreNodeIds", it.joinToString(",")) }
872+
ignoreShortChannelIds?.let { append("ignoreShortChannelIds", it.joinToString(",")) }
873+
format?.let { append("format", it) }
874+
maxFeeMsat?.let { append("maxFeeMsat", it.toString()) }
875+
includeLocalChannelCost?.let { append("includeLocalChannelCost", it.toString()) }
876+
pathFindingExperimentName?.let { append("pathFindingExperimentName", it) }
877+
}
878+
)
879+
when (response.status) {
880+
HttpStatusCode.OK -> Either.Right((response.bodyAsText()))
881+
else -> Either.Left(convertHttpError(response.status))
882+
}
883+
} catch (e: Throwable) {
884+
Either.Left(ApiError(0, e.message ?: "Unknown exception"))
885+
}
886+
}
840887
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package commands
2+
3+
import IResultWriter
4+
import api.IEclairClientBuilder
5+
import arrow.core.flatMap
6+
import kotlinx.cli.ArgType
7+
import kotlinx.coroutines.runBlocking
8+
import types.FindRouteResponse
9+
import types.Serialization
10+
11+
class FindRouteBetweenNodesCommand(
12+
private val resultWriter: IResultWriter,
13+
private val eclairClientBuilder: IEclairClientBuilder
14+
) : BaseCommand(
15+
"findroutebetweennodes",
16+
"Finds a route between two nodes."
17+
) {
18+
private val sourceNodeId by option(
19+
ArgType.String,
20+
description = "The destination of the route"
21+
)
22+
private val targetNodeId by option(
23+
ArgType.String,
24+
description = "The destination of the route"
25+
)
26+
private val amountMsat by option(
27+
ArgType.Int,
28+
description = "The amount that should go through the route"
29+
)
30+
private val ignoreNodeIds by option(
31+
ArgType.String,
32+
description = "A list of nodes to exclude from path-finding"
33+
)
34+
private val ignoreShortChannelIds by option(
35+
ArgType.String,
36+
description = "A list of channels to exclude from path-finding"
37+
)
38+
private val format by option(
39+
ArgType.String,
40+
description = "Format that will be used for the resulting route"
41+
)
42+
private val maxFeeMsat by option(
43+
ArgType.Int,
44+
description = "Maximum fee allowed for this payment"
45+
)
46+
private val includeLocalChannelCost by option(
47+
ArgType.Boolean,
48+
description = "If true, the relay fees of local channels will be counted"
49+
)
50+
private val pathFindingExperimentName by option(
51+
ArgType.String,
52+
description = "Name of the path-finding configuration that should be used"
53+
)
54+
55+
override fun execute() = runBlocking {
56+
val eclairClient = eclairClientBuilder.build(host, password)
57+
val result = eclairClient.findroutebetweennodes(
58+
sourceNodeId!!,
59+
targetNodeId!!,
60+
amountMsat!!,
61+
ignoreNodeIds?.split(","),
62+
ignoreShortChannelIds?.split(","),
63+
format,
64+
maxFeeMsat,
65+
includeLocalChannelCost,
66+
pathFindingExperimentName
67+
).flatMap { apiResponse -> Serialization.decode<FindRouteResponse>(apiResponse) }
68+
.map { decoded -> Serialization.encode(decoded) }
69+
resultWriter.write(result)
70+
}
71+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package commands
2+
3+
import api.IEclairClientBuilder
4+
import kotlinx.cli.ArgParser
5+
import kotlinx.cli.ExperimentalCli
6+
import kotlinx.serialization.json.Json
7+
import mocks.DummyEclairClient
8+
import mocks.DummyResultWriter
9+
import mocks.FailingEclairClient
10+
import types.ApiError
11+
import types.FindRouteResponse
12+
import kotlin.test.*
13+
14+
@OptIn(ExperimentalCli::class)
15+
class FindRouteBetweenNodesCommandTest {
16+
private fun runTest(eclairClient: IEclairClientBuilder): DummyResultWriter {
17+
val resultWriter = DummyResultWriter()
18+
val command = FindRouteBetweenNodesCommand(resultWriter, eclairClient)
19+
val parser = ArgParser("test")
20+
parser.subcommands(command)
21+
parser.parse(
22+
arrayOf(
23+
"findroutebetweennodes",
24+
"-p",
25+
"password",
26+
"--sourceNodeId",
27+
"03c5b161c16e9f8ef3f3bccfb74a6e9a3b423dd41fe2848174b7209f1c2ea25dad",
28+
"--targetNodeId",
29+
"02f666711319435b7905dd77d10c269d8d50c02668b975f526577167d370b50a3e",
30+
"--amountMsat",
31+
"1000"
32+
)
33+
)
34+
return resultWriter
35+
}
36+
37+
@Test
38+
fun `successful request`() {
39+
val resultWriter =
40+
runTest(DummyEclairClient(findroutebetweennodesResponse = DummyEclairClient.validFindRouteBetweenNodesResponse))
41+
assertNull(resultWriter.lastError)
42+
assertNotNull(resultWriter.lastResult)
43+
val format = Json { ignoreUnknownKeys = true }
44+
assertEquals(
45+
format.decodeFromString(
46+
FindRouteResponse.serializer(),
47+
DummyEclairClient.validFindRouteBetweenNodesResponse
48+
),
49+
format.decodeFromString(FindRouteResponse.serializer(), resultWriter.lastResult!!),
50+
)
51+
}
52+
53+
@Test
54+
fun `api error`() {
55+
val error = ApiError(42, "test failure message")
56+
val resultWriter = runTest(FailingEclairClient(error))
57+
assertNull(resultWriter.lastResult)
58+
assertEquals(error, resultWriter.lastError)
59+
}
60+
61+
@Test
62+
fun `serialization error`() {
63+
val resultWriter = runTest(DummyEclairClient(findroutebetweennodesResponse = "{invalidJson}"))
64+
assertNull(resultWriter.lastResult)
65+
assertNotNull(resultWriter.lastError)
66+
assertTrue(resultWriter.lastError!!.message.contains("api response could not be parsed"))
67+
}
68+
}

src/nativeTest/kotlin/mocks/EclairClientMocks.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class DummyEclairClient(
3535
private val listpendinginvoicesResponse: String = validListPendingInvoicesResponse,
3636
private val findrouteResponse: String = validFindRouteResponse,
3737
private val findroutetonodeResponse: String = validFindRouteToNodeResponse,
38+
private val findroutebetweennodesResponse: String = validFindRouteBetweenNodesResponse,
3839
) : IEclairClient, IEclairClientBuilder {
3940
override fun build(apiHost: String, apiPassword: String): IEclairClient = this
4041
override suspend fun getInfo(): Either<ApiError, String> = Either.Right(getInfoResponse)
@@ -193,6 +194,18 @@ class DummyEclairClient(
193194
pathFindingExperimentName: String?
194195
): Either<ApiError, String> = Either.Right(findroutetonodeResponse)
195196

197+
override suspend fun findroutebetweennodes(
198+
sourceNodeId: String,
199+
targetNodeId: String,
200+
amountMsat: Int,
201+
ignoreNodeIds: List<String>?,
202+
ignoreShortChannelIds: List<String>?,
203+
format: String?,
204+
maxFeeMsat: Int?,
205+
includeLocalChannelCost: Boolean?,
206+
pathFindingExperimentName: String?
207+
): Either<ApiError, String> = Either.Right(findroutebetweennodesResponse)
208+
196209
companion object {
197210
val validGetInfoResponse =
198211
"""{"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"}"""
@@ -902,6 +915,18 @@ class DummyEclairClient(
902915
]
903916
}
904917
]
918+
}"""
919+
val validFindRouteBetweenNodesResponse = """{
920+
"routes": [
921+
{
922+
"amount": 5000,
923+
"nodeIds": [
924+
"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96",
925+
"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
926+
"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
927+
]
928+
}
929+
]
905930
}"""
906931
}
907932
}
@@ -1045,4 +1070,16 @@ class FailingEclairClient(private val error: ApiError) : IEclairClient, IEclairC
10451070
includeLocalChannelCost: Boolean?,
10461071
pathFindingExperimentName: String?
10471072
): Either<ApiError, String> = Either.Left(error)
1073+
1074+
override suspend fun findroutebetweennodes(
1075+
sourceNodeId: String,
1076+
targetNodeId: String,
1077+
amountMsat: Int,
1078+
ignoreNodeIds: List<String>?,
1079+
ignoreShortChannelIds: List<String>?,
1080+
format: String?,
1081+
maxFeeMsat: Int?,
1082+
includeLocalChannelCost: Boolean?,
1083+
pathFindingExperimentName: String?
1084+
): Either<ApiError, String> = Either.Left(error)
10481085
}

0 commit comments

Comments
 (0)