Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion 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"
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 common_opts="-p --host"
local connect_opts="--uri --nodeId --address --port"
local disconnect_opts="--nodeId"
Expand All @@ -45,6 +45,7 @@ _eclair_cli() {
local listinvoices_opts="--from --to --count --skip"
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"
# 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 @@ -130,6 +131,9 @@ _eclair_cli() {
findroute)
COMPREPLY=( $(compgen -W "${findroute_opts} ${common_opts}" -- ${cur}) )
;;
findroutetonode)
COMPREPLY=( $(compgen -W "${findroutetonode_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 @@ -36,6 +36,7 @@ fun main(args: Array<String>) {
ListInvoicesCommand(resultWriter, apiClientBuilder),
ListPendingInvoicesCommand(resultWriter, apiClientBuilder),
FindRouteCommand(resultWriter, apiClientBuilder),
FindRouteToNodeCommand(resultWriter, apiClientBuilder),
)
parser.parse(args)
}
45 changes: 45 additions & 0 deletions src/nativeMain/kotlin/api/EclairClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ interface IEclairClient {
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String>


suspend fun findroutetonode(
nodeId: 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 @@ -792,4 +804,37 @@ class EclairClient(private val apiHost: String, private val apiPassword: String)
Either.Left(ApiError(0, e.message ?: "Unknown exception"))
}
}

override suspend fun findroutetonode(
nodeId: 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/findroutetonode",
formParameters = Parameters.build {
append("nodeId", nodeId)
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"))
}
}
}
66 changes: 66 additions & 0 deletions src/nativeMain/kotlin/commands/FindRouteToNode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 FindRouteToNodeCommand(
private val resultWriter: IResultWriter,
private val eclairClientBuilder: IEclairClientBuilder
) : BaseCommand(
"findroutetonode",
"Finds a route to the given node."
) {
private val nodeId 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.findroutetonode(
nodeId!!,
amountMsat!!,
ignoreNodeIds?.split(","),
ignoreShortChannelIds?.split(","),
format,
maxFeeMsat,
includeLocalChannelCost,
pathFindingExperimentName
).flatMap { apiResponse -> Serialization.decode<FindRouteResponse>(apiResponse) }
.map { decoded -> Serialization.encode(decoded) }
resultWriter.write(result)
}
}
63 changes: 63 additions & 0 deletions src/nativeTest/kotlin/commands/FindRouteToNodeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 FindRouteToNodeTestCommandTest {
private fun runTest(eclairClient: IEclairClientBuilder): DummyResultWriter {
val resultWriter = DummyResultWriter()
val command = FindRouteToNodeCommand(resultWriter, eclairClient)
val parser = ArgParser("test")
parser.subcommands(command)
parser.parse(
arrayOf(
"findroutetonode",
"-p",
"password",
"--nodeId",
"02f666711319435b7905dd77d10c269d8d50c02668b975f526577167d370b50a3e",
"--amountMsat",
"1000"
)
)
return resultWriter
}

@Test
fun `successful request`() {
val resultWriter =
runTest(DummyEclairClient(findroutetonodeResponse = DummyEclairClient.validFindRouteToNodeResponse))
assertNull(resultWriter.lastError)
assertNotNull(resultWriter.lastResult)
val format = Json { ignoreUnknownKeys = true }
assertEquals(
format.decodeFromString(FindRouteResponse.serializer(), DummyEclairClient.validFindRouteToNodeResponse),
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(findroutetonodeResponse = "{invalidJson}"))
assertNull(resultWriter.lastResult)
assertNotNull(resultWriter.lastError)
assertTrue(resultWriter.lastError!!.message.contains("api response could not be parsed"))
}
}
35 changes: 35 additions & 0 deletions src/nativeTest/kotlin/mocks/EclairClientMocks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class DummyEclairClient(
private val listinvoicesResponse: String = validListInvoicesResponse,
private val listpendinginvoicesResponse: String = validListPendingInvoicesResponse,
private val findrouteResponse: String = validFindRouteResponse,
private val findroutetonodeResponse: String = validFindRouteToNodeResponse,
) : 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 @@ -181,6 +182,17 @@ class DummyEclairClient(
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Right(findrouteResponse)

override suspend fun findroutetonode(
nodeId: String,
amountMsat: Int,
ignoreNodeIds: List<String>?,
ignoreShortChannelIds: List<String>?,
format: String?,
maxFeeMsat: Int?,
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Right(findroutetonodeResponse)

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 @@ -879,6 +891,18 @@ class DummyEclairClient(
]
}
"""
val validFindRouteToNodeResponse = """{
Copy link
Member

Choose a reason for hiding this comment

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

You're only testing the case where format=nodeId, you need to also test the format=shortChannelId and format=full cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I'll cover them soon.

"routes": [
{
"amount": 5000,
"nodeIds": [
"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96",
"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
]
}
]
}"""
}
}

Expand Down Expand Up @@ -1010,4 +1034,15 @@ class FailingEclairClient(private val error: ApiError) : IEclairClient, IEclairC
includeLocalChannelCost: Boolean?,
pathFindingExperimentName: String?
): Either<ApiError, String> = Either.Left(error)

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