diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index 243c93c..84352ff 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -113,8 +113,16 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va post("createinvoice") { val formParameters = call.receiveParameters() val amount = formParameters.getOptionalLong("amountSat")?.sat - val description = formParameters.getString("description") - val invoice = peer.createInvoice(randomBytes32(), amount?.toMilliSatoshi(), Either.Left(description)) + val maxDescriptionSize = 128 + val description = formParameters["description"] + ?.also { if (it.length > maxDescriptionSize) badRequest("Request parameter description is too long (max $maxDescriptionSize characters)") } + val descriptionHash = formParameters.getOptionalByteVector32("descriptionHash") + val eitherDesc = when { + description != null && descriptionHash == null -> Either.Left(description) + description == null && descriptionHash != null -> Either.Right(descriptionHash) + else -> badRequest("Must provide either a description or descriptionHash") + } + val invoice = peer.createInvoice(randomBytes32(), amount?.toMilliSatoshi(), eitherDesc) formParameters["externalId"]?.takeUnless { it.isBlank() }?.let { externalId -> paymentDb.metadataQueries.insertExternalId(WalletPaymentId.IncomingPaymentId(invoice.paymentHash), externalId) } @@ -228,13 +236,17 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va private fun invalidType(argName: String, typeName: String): Nothing = throw ParameterConversionException(argName, typeName) + private fun badRequest(message: String): Nothing = throw BadRequestException(message) + private fun Parameters.getString(argName: String): String = (this[argName] ?: missing(argName)) private fun Parameters.getByteVector32(argName: String): ByteVector32 = getString(argName).let { hex -> kotlin.runCatching { ByteVector32.fromValidHex(hex) }.getOrNull() ?: invalidType(argName, "hex32") } + private fun Parameters.getOptionalByteVector32(argName: String): ByteVector32? = this[argName]?.let { hex -> kotlin.runCatching { ByteVector32.fromValidHex(hex) }.getOrNull() ?: invalidType(argName, "hex32") } + private fun Parameters.getUUID(argName: String): UUID = getString(argName).let { uuid -> kotlin.runCatching { UUID.fromString(uuid) }.getOrNull() ?: invalidType(argName, "uuid") } - private fun Parameters.getAddressAndConvertToScript(argName: String): ByteVector = Script.write(Bitcoin.addressToPublicKeyScript(nodeParams.chainHash, getString(argName)).right ?: error("invalid address")).toByteVector() + private fun Parameters.getAddressAndConvertToScript(argName: String): ByteVector = Script.write(Bitcoin.addressToPublicKeyScript(nodeParams.chainHash, getString(argName)).right ?: badRequest("Invalid address")).toByteVector() private fun Parameters.getInvoice(argName: String): Bolt11Invoice = getString(argName).let { invoice -> Bolt11Invoice.read(invoice).getOrElse { invalidType(argName, "bolt11invoice") } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt index 75fa57c..5b1974e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt @@ -5,6 +5,9 @@ import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.output.MordantHelpFormatter +import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions +import com.github.ajalt.clikt.parameters.groups.required +import com.github.ajalt.clikt.parameters.groups.single import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.long @@ -12,6 +15,7 @@ import com.github.ajalt.clikt.sources.MapValueSource import fr.acinq.bitcoin.Base58Check import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.BuildVersions import fr.acinq.lightning.bin.conf.readConfFile import fr.acinq.lightning.bin.datadir @@ -124,7 +128,7 @@ class GetOutgoingPayment : PhoenixCliCommand(name = "getoutgoingpayment", help = } class GetIncomingPayment : PhoenixCliCommand(name = "getincomingpayment", help = "Get incoming payment") { - private val paymentHash by option("--paymentHash", "--h").convert { ByteVector32.fromValidHex(it) }.required() + private val paymentHash by option("--paymentHash", "--h").convert { it.toByteVector32() }.required() override suspend fun httpRequest() = commonOptions.httpClient.use { it.get(url = commonOptions.baseUrl / "payments/incoming/$paymentHash") } @@ -143,7 +147,11 @@ class ListIncomingPayments : PhoenixCliCommand(name = "listincomingpayments", he class CreateInvoice : PhoenixCliCommand(name = "createinvoice", help = "Create a Lightning invoice", printHelpOnEmptyArgs = true) { private val amountSat by option("--amountSat").long() - private val description by option("--description", "--desc").required() + private val description by mutuallyExclusiveOptions( + option("--description", "--desc").convert { Either.Left(it) }, + option("--descriptionHash", "--desc-hash").convert { Either.Right(it.toByteVector32()) } + ).single().required() + private val externalId by option("--externalId") override suspend fun httpRequest() = commonOptions.httpClient.use { it.submitForm( @@ -151,7 +159,10 @@ class CreateInvoice : PhoenixCliCommand(name = "createinvoice", help = "Create a formParameters = parameters { amountSat?.let { append("amountSat", it.toString()) } externalId?.let { append("externalId", it) } - append("description", description) + when(val d = description) { + is Either.Left -> append("description", d.value) + is Either.Right -> append("descriptionHash", d.value.toHex()) + } } ) } @@ -188,7 +199,7 @@ class SendToAddress : PhoenixCliCommand(name = "sendtoaddress", help = "Send to } class CloseChannel : PhoenixCliCommand(name = "closechannel", help = "Close channel", printHelpOnEmptyArgs = true) { - private val channelId by option("--channelId").convert { ByteVector32.fromValidHex(it) }.required() + private val channelId by option("--channelId").convert { it.toByteVector32() }.required() private val address by option("--address").required().check { runCatching { Base58Check.decode(it) }.isSuccess || runCatching { Bech32.decodeWitnessAddress(it) }.isSuccess } private val feerateSatByte by option("--feerateSatByte").int().required() override suspend fun httpRequest() = commonOptions.httpClient.use { @@ -203,4 +214,6 @@ class CloseChannel : PhoenixCliCommand(name = "closechannel", help = "Close chan } } -operator fun Url.div(path: String) = Url(URLBuilder(this).appendPathSegments(path)) \ No newline at end of file +operator fun Url.div(path: String) = Url(URLBuilder(this).appendPathSegments(path)) + +fun String.toByteVector32(): ByteVector32 = kotlin.runCatching { ByteVector32.fromValidHex(this) }.recover { error("'$this' is not a valid 32-bytes hex string") }.getOrThrow() \ No newline at end of file