From d984c934b6d156e7b8520d052bb670b2cfa1d8e3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 5 Jul 2022 16:21:51 -0300 Subject: [PATCH] fix pay-lnurl, use IO.async_ instead of Dispatcher whenever possible + some other minor quibbles. --- build.sbt | 4 +- native-image-configs/reflect-config.json | 135 ++++---- src/main/scala/Commands.scala | 294 ++++++++++++------ src/main/scala/Handler.scala | 12 + src/main/scala/utils/ConnectionProvider.scala | 6 +- 5 files changed, 281 insertions(+), 170 deletions(-) diff --git a/build.sbt b/build.sbt index 30d8754..6fa0f5f 100644 --- a/build.sbt +++ b/build.sbt @@ -3,9 +3,9 @@ enablePlugins(NativeImagePlugin) name := "cliche" organization := "fiatjaf" scalaVersion := "2.13.8" -version := "0.5.0" +version := "0.4.4" libraryDependencies ++= Seq( - "com.fiatjaf" % "immortan_2.13" % "0.7.2-SNAPSHOT", + "com.fiatjaf" % "immortan_2.13" % "0.7.2", "com.github.alexarchambault" % "case-app_2.13" % "2.1.0-M13", "com.lihaoyi" % "requests_2.13" % "0.7.0", "com.iheart" % "ficus_2.13" % "1.5.0", diff --git a/native-image-configs/reflect-config.json b/native-image-configs/reflect-config.json index 23933cd..05e842a 100644 --- a/native-image-configs/reflect-config.json +++ b/native-image-configs/reflect-config.json @@ -34,6 +34,11 @@ "allDeclaredFields":true, "queryAllPublicMethods":true }, +{ + "name":"PayLnurl", + "allDeclaredFields":true, + "queryAllPublicMethods":true +}, { "name":"RemoveHostedChannel", "allDeclaredFields":true, @@ -208,8 +213,8 @@ { "name":"fr.acinq.eclair.blockchain.electrum.ElectrumClient$ExceptionHandler", "queriedMethods":[ - {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] } ] }, @@ -219,7 +224,7 @@ { "name":"fr.acinq.eclair.blockchain.electrum.ElectrumClient$ResponseHandler", "queriedMethods":[ - {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } ] }, @@ -238,11 +243,11 @@ { "name":"io.netty.channel.ChannelDuplexHandler", "queriedMethods":[ - {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] } ] }, @@ -253,69 +258,69 @@ { "name":"io.netty.channel.ChannelInboundHandlerAdapter", "queriedMethods":[ - {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, - {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } ] }, { "name":"io.netty.channel.ChannelInitializer", "queriedMethods":[ - {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] } ] }, { "name":"io.netty.channel.ChannelOutboundHandlerAdapter", "queriedMethods":[ - {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] } ] }, { "name":"io.netty.channel.DefaultChannelPipeline$HeadContext", "queriedMethods":[ - {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, - {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, - {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] } ] }, { "name":"io.netty.channel.DefaultChannelPipeline$TailContext", "queriedMethods":[ - {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, - {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } ] }, @@ -326,9 +331,9 @@ { "name":"io.netty.handler.codec.ByteToMessageDecoder", "queriedMethods":[ - {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, - {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } ] }, @@ -352,17 +357,17 @@ { "name":"io.netty.handler.ssl.SslHandler", "queriedMethods":[ - {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, - {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, - {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, - {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, - {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] } ] }, @@ -496,7 +501,7 @@ "name":"java.lang.management.RuntimeMXBean", "queryAllPublicMethods":true, "methods":[ - {"name":"getInputArguments","parameterTypes":[] }, + {"name":"getInputArguments","parameterTypes":[] }, {"name":"getName","parameterTypes":[] } ] }, @@ -672,7 +677,7 @@ { "name":"sun.misc.Signal", "methods":[ - {"name":"","parameterTypes":["java.lang.String"] }, + {"name":"","parameterTypes":["java.lang.String"] }, {"name":"handle","parameterTypes":["sun.misc.Signal","sun.misc.SignalHandler"] } ] }, @@ -684,14 +689,14 @@ "fields":[{"name":"theUnsafe"}], "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }], "queriedMethods":[ - {"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] }, + {"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] }, {"name":"getAndSetObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] } ] }, { "name":"sun.nio.ch.SelectorImpl", "fields":[ - {"name":"publicSelectedKeys"}, + {"name":"publicSelectedKeys"}, {"name":"selectedKeys"} ] }, diff --git a/src/main/scala/Commands.scala b/src/main/scala/Commands.scala index a4e7f75..5721654 100644 --- a/src/main/scala/Commands.scala +++ b/src/main/scala/Commands.scala @@ -30,9 +30,10 @@ import immortan.{ PaymentInfo, CommsTower } -import immortan.utils.{PaymentRequestExt, BitcoinUri} +import immortan.utils.{PayRequest, PaymentRequestExt, BitcoinUri, LNUrl} import immortan.crypto.Tools.{~} import scodec.bits.ByteVector +import immortan.utils.InputParser sealed trait JSONRPCMessage { def render(forceCompact: Boolean = false): String = { @@ -130,7 +131,7 @@ object Commands { val localParams = LNParams.makeChannelParams(isFunder = false, LNParams.minChanDustLimit) - (( + ( ByteVector.fromHex(params.pubkey), Try( RemoteNodeInfo( @@ -141,86 +142,71 @@ object Commands { ).toEither ) match { case (Some(pubkey), _) if pubkey.length != 33 => - topic.publish1(JSONRPCError(id, "pubkey must be 33 bytes hex")) - case (None, _) => topic.publish1(JSONRPCError(id, ("invalid pubkey hex"))) + topic.publish1( + JSONRPCError(id, "pubkey must be 33 bytes hex") + ) >> IO.unit + case (None, _) => + topic.publish1(JSONRPCError(id, ("invalid pubkey hex"))) >> IO.unit case (_, Left(_)) => - topic.publish1(JSONRPCError(id, ("invalid node address or port"))) + topic.publish1( + JSONRPCError(id, ("invalid node address or port")) + ) >> IO.unit case (Some(pubkey), Right(target)) => - Dispatcher[IO].use { dispatcher => - for { - blocker <- CountDownLatch[IO](1) - _ <- IO.delay { - val cancel = dispatcher.unsafeRunCancelable( - IO.sleep(FiniteDuration(5, "seconds")) >> - topic.publish1( - JSONRPCError(id, "taking too long") - ) >> blocker.release - ) + for { + response <- IO.async_[JSONRPCMessage] { cb => + new HCOpenHandler( + target, + randomBytes32, + localParams.defaultFinalScriptPubKey, + LNParams.cm + ) { + def onException: Unit = { + cb(Right(JSONRPCError(id, "exception"))) + } - new HCOpenHandler( - target, - randomBytes32, - localParams.defaultFinalScriptPubKey, - LNParams.cm - ) { - def onException: Unit = { - dispatcher.unsafeRunAndForget( - topic.publish1(JSONRPCError(id, "exception")) - ) - dispatcher.unsafeRunAndForget(blocker.release) - cancel() - } - - // stop automatic HC opening attempts on getting any kind of local/remote error, - // this won't be triggered on disconnect - def onFailure(reason: Throwable) = { - dispatcher.unsafeRunAndForget( - topic.publish1(JSONRPCError(id, reason.toString())) - ) - dispatcher.unsafeRunAndForget(blocker.release) - cancel() - } - - def onEstablished( - cs: Commitments, - freshChannel: ChannelHosted - ) = { - cancel() - dispatcher.unsafeRunAndForget( - topic.publish1( - JSONRPCResponse( - id, - ( - // @formatter:off - ("channel_id" -> cs.channelId.toHex) ~~ - ("peer" -> - (("pubkey" -> cs.remoteInfo.nodeId.toString)) ~~ - ("our_pubkey" -> cs.remoteInfo.nodeSpecificPubKey.toString) ~~ - ("addr" -> cs.remoteInfo.address.toString()) - ) - // @formatter:on + // stop automatic HC opening attempts on getting any kind of local/remote error, + // this won't be triggered on disconnect + def onFailure(reason: Throwable) = { + cb(Right(JSONRPCError(id, reason.toString()))) + } + + def onEstablished( + cs: Commitments, + freshChannel: ChannelHosted + ) = { + cb( + Right( + JSONRPCResponse( + id, + ( + // @formatter:off + ("channel_id" -> cs.channelId.toHex) ~~ + ("peer" -> + (("pubkey" -> cs.remoteInfo.nodeId.toString)) ~~ + ("our_pubkey" -> cs.remoteInfo.nodeSpecificPubKey.toString) ~~ + ("addr" -> cs.remoteInfo.address.toString()) ) + // @formatter:on ) ) ) - dispatcher.unsafeRunAndForget(blocker.release) + ) - LNParams.cm.pf process PathFinder.CMDStartPeriodicResync - LNParams.cm.all += Tuple2(cs.channelId, freshChannel) + LNParams.cm.pf process PathFinder.CMDStartPeriodicResync + LNParams.cm.all += Tuple2(cs.channelId, freshChannel) - // this removes all previous channel listeners - freshChannel.listeners = Set(LNParams.cm) - LNParams.cm.initConnect() + // this removes all previous channel listeners + freshChannel.listeners = Set(LNParams.cm) + LNParams.cm.initConnect() - // update view on hub activity and finalize local stuff - ChannelMaster.next(ChannelMaster.statusUpdateStream) - } + // update view on hub activity and finalize local stuff + ChannelMaster.next(ChannelMaster.statusUpdateStream) } } - _ <- blocker.await - } yield () - } - }) >> IO.unit + } + _ <- topic.publish1(response) + } yield () + } } def removeHC( @@ -246,7 +232,7 @@ object Commands { topic.publish1( JSONRPCError( id, - s"invalid or unknown channel id ${params.id}" + s"invalid or unknown channel id ${params.channelId}" ) ) >> IO.unit } @@ -433,6 +419,117 @@ object Commands { }) >> IO.unit } + def payLnurl(params: PayLnurl)(implicit + id: String, + topic: Topic[IO, JSONRPCMessage] + ): IO[Unit] = { + Dispatcher[IO].use { dispatcher => + for { + dummyTopic <- Topic[IO, JSONRPCMessage] + blocker <- CountDownLatch[IO](1) + _ <- IO.delay { + def onBadResponse(err: Throwable): Unit = { + try { + dispatcher.unsafeRunAndForget( + topic.publish1( + JSONRPCError(id, err.toString) + ) >> dummyTopic.close >> blocker.release + ) + } catch { + case _: Throwable => {} + } + } + + Try(InputParser.parse(params.lnurl)) match { + case Success(lnurl: LNUrl) => { + lnurl.level1DataResponse.subscribe( + { + case lnurlpay: PayRequest => { + dispatcher.unsafeRunAndForget( + topic.publish1( + JSONRPCNotification( + "lnurlpay_params", + ( + // @formatter:off + ("min" -> lnurlpay.minSendable) ~~ + ("max" -> lnurlpay.maxSendable) ~~ + ("callback" -> lnurlpay.callback) ~~ + ("description" -> lnurlpay.meta.textShort) ~~ + ("comment" -> lnurlpay.commentAllowed) ~~ + ("payerdata" -> lnurlpay.payerData.map(pds => + (("auth", pds.auth.isDefined) ~~ + ("name", pds.name.isDefined) ~~ + ("pubkey", pds.pubkey.isDefined)) + )) + // @formatter:on + ) + ) + ) + ) + + if ( + lnurlpay.maxSendable < params.msatoshi || lnurlpay.minSendable > params.msatoshi + ) { + dispatcher.unsafeRunAndForget( + topic.publish1( + JSONRPCError( + id, + s"amount provided (${params.msatoshi}) is not in the acceptable range ${lnurlpay.minSendable}-${lnurlpay.maxSendable}" + ) + ) >> dummyTopic.close >> blocker.release + ) + } else { + lnurlpay + .getFinal( + amount = MilliSatoshi(params.msatoshi), + comment = params.comment, + name = params.name, + authKeyHost = + if (params.attachAuth) Some(lnurl.uri.getHost) + else None + ) + .subscribe( + { lnurlpayfinal => + dispatcher.unsafeRunAndForget( + payInvoice( + PayInvoice( + invoice = lnurlpayfinal.pr, + msatoshi = None + ) + )("", dummyTopic) >> blocker.release + ) + }, + onBadResponse + ) + } + } + case _ => { + dispatcher.unsafeRunAndForget( + topic.publish1( + JSONRPCError( + id, + "got something that isn't a valid lnurl-pay response" + ) + ) >> dummyTopic.close >> blocker.release + ) + } + }, + onBadResponse + ) + } + case _ => { + dispatcher.unsafeRunAndForget( + topic.publish1(JSONRPCError(id, "invalid lnurl")) + >> dummyTopic.close >> blocker.release + ) + } + } + } + _ <- blocker.await + } yield () + } + } + def checkPayment(params: CheckPayment)(implicit id: String, topic: Topic[IO, JSONRPCMessage] @@ -480,39 +577,32 @@ object Commands { implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global - Dispatcher[IO].use { dispatcher => - for { - blocker <- CountDownLatch[IO](1) - _ <- IO.delay { - LNParams.chainWallets.wallets.head.getReceiveAddresses - .onComplete { - case Success(resp) => { - val address = resp.keys - .take(1) - .map(resp.ewt.textAddress) - .head - - dispatcher.unsafeRunAndForget( - topic.publish1( - JSONRPCResponse( - id, - ("address" -> address) - ) + for { + response <- IO.async_[JSONRPCMessage] { cb => + LNParams.chainWallets.wallets.head.getReceiveAddresses + .onComplete { + case Success(resp) => { + val address = resp.keys + .take(1) + .map(resp.ewt.textAddress) + .head + + cb( + Right( + JSONRPCResponse( + id, + ("address" -> address) ) ) - dispatcher.unsafeRunAndForget(blocker.release) - } - case Failure(err) => { - dispatcher.unsafeRunAndForget( - topic.publish1(JSONRPCError(id, err.toString())) - ) - dispatcher.unsafeRunAndForget(blocker.release) - } + ) } - } - _ <- blocker.await - } yield () - } + case Failure(err) => { + cb(Right(JSONRPCError(id, err.toString()))) + } + } + } + _ <- topic.publish1(response) + } yield () } def sendToAddress( @@ -560,7 +650,7 @@ object Commands { ("hosted_channel" -> (("override_proposal" -> ("their_balance" -> hc.overrideProposal.map(_.localBalanceMsat.toLong)) ~~ - ("our_balance" -> hc.overrideProposal.map(_.remoteBalanceMsat.toLong)) + ("our_balance" -> hc.overrideProposal.map(op => hc.lastCrossSignedState.initHostedChannel.channelCapacityMsat.toLong - op.localBalanceMsat.toLong)) ) ~~ ("resize_proposal" -> hc.resizeProposal.map(_.newCapacity.toLong * 1000))) ) diff --git a/src/main/scala/Handler.scala b/src/main/scala/Handler.scala index 2a5e054..4e0c218 100644 --- a/src/main/scala/Handler.scala +++ b/src/main/scala/Handler.scala @@ -23,6 +23,13 @@ case class CreateInvoice( label: Option[String] ) extends Command case class PayInvoice(invoice: String, msatoshi: Option[Long]) extends Command +case class PayLnurl( + lnurl: String, + msatoshi: Long, + comment: Option[String], + name: Option[String], + attachAuth: Boolean = true +) extends Command case class CheckPayment(hash: String) extends Command case class ListPayments(count: Option[Int]) extends Command case class GetAddress() extends Command @@ -65,6 +72,7 @@ object Handler { case "request-hc" => (id, params.convertTo[RequestHostedChannel]) case "create-invoice" => (id, params.convertTo[CreateInvoice]) case "pay-invoice" => (id, params.convertTo[PayInvoice]) + case "pay-lnurl" => (id, params.convertTo[PayLnurl]) case "check-payment" => (id, params.convertTo[CheckPayment]) case "list-payments" => (id, params.convertTo[ListPayments]) case "remove-hc" => (id, params.convertTo[RemoveHostedChannel]) @@ -89,6 +97,7 @@ object Handler { case "request-hc" => CaseApp.parse[RequestHostedChannel](tail) case "create-invoice" => CaseApp.parse[CreateInvoice](tail) case "pay-invoice" => CaseApp.parse[PayInvoice](tail) + case "pay-lnurl" => CaseApp.parse[PayLnurl](tail) case "check-payment" => CaseApp.parse[CheckPayment](tail) case "list-payments" => CaseApp.parse[ListPayments](tail) case "remove-hc" => CaseApp.parse[RemoveHostedChannel](tail) @@ -118,6 +127,7 @@ object Handler { case params: RemoveHostedChannel => Commands.removeHC(params) case params: CreateInvoice => Commands.createInvoice(params) case params: PayInvoice => Commands.payInvoice(params) + case params: PayLnurl => Commands.payLnurl(params) case params: CheckPayment => Commands.checkPayment(params) case params: ListPayments => Commands.listPayments(params) case params: AcceptOverride => Commands.acceptOverride(params) @@ -147,6 +157,8 @@ object SprayConverters extends DefaultJsonProtocol { jsonFormat5(CreateInvoice.apply) implicit val convertPayInvoice: JsonFormat[PayInvoice] = jsonFormat2(PayInvoice.apply) + implicit val convertPayLnurl: JsonFormat[PayLnurl] = + jsonFormat5(PayLnurl.apply) implicit val convertCheckPayment: JsonFormat[CheckPayment] = jsonFormat1(CheckPayment.apply) implicit val convertListPayments: JsonFormat[ListPayments] = diff --git a/src/main/scala/utils/ConnectionProvider.scala b/src/main/scala/utils/ConnectionProvider.scala index 7d46a16..ca06aee 100644 --- a/src/main/scala/utils/ConnectionProvider.scala +++ b/src/main/scala/utils/ConnectionProvider.scala @@ -8,5 +8,9 @@ class RequestsConnectionProvider extends ConnectionProvider { override val proxyAddress: Option[InetSocketAddress] = Option.empty override def doWhenReady(action: => Unit): Unit = action override def getSocket: Socket = new Socket - override def get(url: String): String = requests.get(url).text() + override def get(url: String): String = try { + requests.get(url).text() + } catch { + case exc: requests.RequestFailedException => exc.response.data.toString + } }