From 42481c66e6752a2aff274c7e71960d4e99308ffd Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 28 Sep 2020 09:51:42 +0200 Subject: [PATCH] Add some channel events to websocket (#1536) Send basic channel events to websockets listeners: * Channel open initiated * Channel state change * Channel closed We only send basic, high-level data about these events. If the listener is interested in details, it should call the `channelInfo` API to get all of the channel's data. Fixes #1509 --- .../fr/acinq/eclair/api/JsonSerializers.scala | 34 +++++++++++++++++-- .../scala/fr/acinq/eclair/api/Service.scala | 10 ++++++ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 22 +++++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index f03b75609..ff701dfc0 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.{ChannelOpenResponse, ChannelVersion, CloseCommand, Command, CommandResponse, RES_SUCCESS, State} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} import fr.acinq.eclair.payment._ @@ -42,6 +42,7 @@ import scodec.bits.ByteVector * JSON Serializers. * Note: in general, deserialization does not need to be implemented. */ + class ByteVectorSerializer extends CustomSerializer[ByteVector](_ => ( { null }, { @@ -278,6 +279,30 @@ class JavaUUIDSerializer extends CustomSerializer[UUID](_ => ( { case id: UUID => JString(id.toString) })) +class ChannelEventSerializer extends CustomSerializer[ChannelEvent](_ => ( { + null +}, { + case e: ChannelCreated => JObject( + JField("type", JString("channel-opened")), + JField("remoteNodeId", JString(e.remoteNodeId.toString())), + JField("isFunder", JBool(e.isFunder)), + JField("temporaryChannelId", JString(e.temporaryChannelId.toHex)), + JField("initialFeeratePerKw", JLong(e.initialFeeratePerKw.toLong)), + JField("fundingTxFeeratePerKw", e.fundingTxFeeratePerKw.map(f => JLong(f.toLong)).getOrElse(JNothing)) + ) + case e: ChannelStateChanged => JObject( + JField("type", JString("channel-state-changed")), + JField("remoteNodeId", JString(e.remoteNodeId.toString())), + JField("previousState", JString(e.previousState.toString)), + JField("currentState", JString(e.currentState.toString)) + ) + case e: ChannelClosed => JObject( + JField("type", JString("channel-closed")), + JField("channelId", JString(e.channelId.toHex)), + JField("closingType", JString(e.closingType.getClass.getSimpleName)) + ) +})) + case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { val reverse: Map[String, Class[_]] = custom.map(_.swap) @@ -321,6 +346,7 @@ object JsonSupport extends Json4sSupport { new ByteVectorSerializer + new ByteVector32Serializer + new ByteVector64Serializer + + new ChannelEventSerializer + new UInt64Serializer + new SatoshiSerializer + new MilliSatoshiSerializer + @@ -360,10 +386,12 @@ object JsonSupport extends Json4sSupport { JObject( JField("name", JString(a.feature.rfcName)), JField("support", JString(a.support.toString)) - )}.toList)), + ) + }.toList)), JField("unknown", JArray(features.unknown.map { i => JObject( JField("featureBit", JInt(i.bitIndex)) - )}.toList)) + ) + }.toList)) ) } \ No newline at end of file diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala index abc647d8d..f28382050 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -35,6 +35,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.api.FormParamExtractors._ import fr.acinq.eclair.blockchain.fee.FeeratePerByte +import fr.acinq.eclair.channel.{ChannelClosed, ChannelCreated, ChannelEvent, ChannelStateChanged, WAIT_FOR_INIT_INTERNAL} import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.{PaymentEvent, PaymentRequest} import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi} @@ -91,10 +92,19 @@ trait Service extends ExtraDirectives with Logging { override def preStart: Unit = { context.system.eventStream.subscribe(self, classOf[PaymentEvent]) + context.system.eventStream.subscribe(self, classOf[ChannelCreated]) + context.system.eventStream.subscribe(self, classOf[ChannelStateChanged]) + context.system.eventStream.subscribe(self, classOf[ChannelClosed]) } def receive: Receive = { case message: PaymentEvent => flowInput.offer(serialization.write(message)) + case message: ChannelCreated => flowInput.offer(serialization.write(message)) + case message: ChannelStateChanged => + if (message.previousState != WAIT_FOR_INIT_INTERNAL) { + flowInput.offer(serialization.write(message)) + } + case message: ChannelClosed => flowInput.offer(serialization.write(message)) } })) diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 97b5c0d93..e44895b52 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -33,7 +33,9 @@ import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, OptionDataLossProtect} import fr.acinq.eclair._ -import fr.acinq.eclair.channel.{CMD_CLOSE, ChannelOpenResponse, CommandResponse, RES_SUCCESS} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.Helpers.Closing +import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.io.Peer.PeerInfo @@ -530,6 +532,24 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(serialization.write(pset) === expectedSerializedPset) system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset) + + val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isFunder = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) + val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isFunder":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","initialFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" + assert(serialization.write(chcr) === expectedSerializedChcr) + system.eventStream.publish(chcr) + wsClient.expectMessage(expectedSerializedChcr) + + val chsc = ChannelStateChanged(system.deadLetters, system.deadLetters, bobNodeId, OFFLINE, NORMAL, null) + val expectedSerializedChsc = """{"type":"channel-state-changed","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","previousState":"OFFLINE","currentState":"NORMAL"}""" + assert(serialization.write(chsc) === expectedSerializedChsc) + system.eventStream.publish(chsc) + wsClient.expectMessage(expectedSerializedChsc) + + val chcl = ChannelClosed(system.deadLetters, ByteVector32.One, Closing.NextRemoteClose(null, null), null) + val expectedSerializedChcl = """{"type":"channel-closed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","closingType":"NextRemoteClose"}""" + assert(serialization.write(chcl) === expectedSerializedChcl) + system.eventStream.publish(chcl) + wsClient.expectMessage(expectedSerializedChcl) } }