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 f0cb3d4442..4e84fabd3f 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,8 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.channel.{ChannelVersion, State} import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} +import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.RouteResponse import fr.acinq.eclair.transactions.Direction import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} @@ -36,100 +37,144 @@ import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson} import scodec.bits.ByteVector /** - * JSON Serializers. - * Note: in general, deserialization does not need to be implemented. - */ -class ByteVectorSerializer extends CustomSerializer[ByteVector](format => ({ null }, { + * JSON Serializers. + * Note: in general, deserialization does not need to be implemented. + */ +class ByteVectorSerializer extends CustomSerializer[ByteVector](_ => ( { + null +}, { case x: ByteVector => JString(x.toHex) })) -class ByteVector32Serializer extends CustomSerializer[ByteVector32](format => ({ null }, { +class ByteVector32Serializer extends CustomSerializer[ByteVector32](_ => ( { + null +}, { case x: ByteVector32 => JString(x.toHex) })) -class ByteVector64Serializer extends CustomSerializer[ByteVector64](format => ({ null }, { +class ByteVector64Serializer extends CustomSerializer[ByteVector64](_ => ( { + null +}, { case x: ByteVector64 => JString(x.toHex) })) -class UInt64Serializer extends CustomSerializer[UInt64](format => ({ null }, { +class UInt64Serializer extends CustomSerializer[UInt64](_ => ( { + null +}, { case x: UInt64 => JInt(x.toBigInt) })) -class SatoshiSerializer extends CustomSerializer[Satoshi](format => ({ null }, { +class SatoshiSerializer extends CustomSerializer[Satoshi](_ => ( { + null +}, { case x: Satoshi => JInt(x.toLong) })) -class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ({ null }, { +class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](_ => ( { + null +}, { case x: MilliSatoshi => JInt(x.toLong) })) -class CltvExpirySerializer extends CustomSerializer[CltvExpiry](format => ({ null }, { +class CltvExpirySerializer extends CustomSerializer[CltvExpiry](_ => ( { + null +}, { case x: CltvExpiry => JLong(x.toLong) })) -class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](format => ({ null }, { +class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](_ => ( { + null +}, { case x: CltvExpiryDelta => JInt(x.toInt) })) -class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ({ null }, { - case x: ShortChannelId => JString(x.toString()) +class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](_ => ( { + null +}, { + case x: ShortChannelId => JString(x.toString) })) -class StateSerializer extends CustomSerializer[State](format => ({ null }, { - case x: State => JString(x.toString()) +class StateSerializer extends CustomSerializer[State](_ => ( { + null +}, { + case x: State => JString(x.toString) })) -class ShaChainSerializer extends CustomSerializer[ShaChain](format => ({ null }, { - case x: ShaChain => JNull +class ShaChainSerializer extends CustomSerializer[ShaChain](_ => ( { + null +}, { + case _: ShaChain => JNull })) -class PublicKeySerializer extends CustomSerializer[PublicKey](format => ({ null }, { +class PublicKeySerializer extends CustomSerializer[PublicKey](_ => ( { + null +}, { case x: PublicKey => JString(x.toString()) })) -class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ({ null }, { - case x: PrivateKey => JString("XXX") +class PrivateKeySerializer extends CustomSerializer[PrivateKey](_ => ( { + null +}, { + case _: PrivateKey => JString("XXX") })) -class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](format => ({ null }, { +class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](_ => ( { + null +}, { case x: ChannelVersion => JString(x.bits.toBin) })) -class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ({ null }, { +class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( { + null +}, { case x: Transaction => JObject(List( JField("txid", JString(x.txid.toHex)), JField("tx", JString(x.toString())) )) })) -class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ({ null }, { +class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( { + null +}, { case x: TransactionWithInputInfo => JObject(List( JField("txid", JString(x.tx.txid.toHex)), JField("tx", JString(x.tx.toString())) )) })) -class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ({ null }, { +class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](_ => ( { + null +}, { case address: InetSocketAddress => JString(HostAndPort.fromParts(address.getHostString, address.getPort).toString) })) -class OutPointSerializer extends CustomSerializer[OutPoint](format => ({ null }, { +class OutPointSerializer extends CustomSerializer[OutPoint](_ => ( { + null +}, { case x: OutPoint => JString(s"${x.txid}:${x.index}") })) -class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ({ null }, { +class OutPointKeySerializer extends CustomKeySerializer[OutPoint](_ => ( { + null +}, { case x: OutPoint => s"${x.txid}:${x.index}" })) -class InputInfoSerializer extends CustomSerializer[InputInfo](format => ({ null }, { +class InputInfoSerializer extends CustomSerializer[InputInfo](_ => ( { + null +}, { case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.toLong))) })) -class ColorSerializer extends CustomSerializer[Color](format => ({ null }, { +class ColorSerializer extends CustomSerializer[Color](_ => ( { + null +}, { case c: Color => JString(c.toString) })) -class RouteResponseSerializer extends CustomSerializer[RouteResponse](format => ({ null }, { +class RouteResponseSerializer extends CustomSerializer[RouteResponse](_ => ( { + null +}, { case route: RouteResponse => val nodeIds = route.hops match { case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId @@ -138,57 +183,98 @@ class RouteResponseSerializer extends CustomSerializer[RouteResponse](format => JArray(nodeIds.toList.map(n => JString(n.toString))) })) -class ThrowableSerializer extends CustomSerializer[Throwable](format => ({ null }, { +class ThrowableSerializer extends CustomSerializer[Throwable](_ => ( { + null +}, { case t: Throwable if t.getMessage != null => JString(t.getMessage) case t: Throwable => JString(t.getClass.getSimpleName) })) -class FailureMessageSerializer extends CustomSerializer[FailureMessage](format => ({ null }, { +class FailureMessageSerializer extends CustomSerializer[FailureMessage](_ => ( { + null +}, { case m: FailureMessage => JString(m.message) })) -class NodeAddressSerializer extends CustomSerializer[NodeAddress](format => ({ null},{ +class NodeAddressSerializer extends CustomSerializer[NodeAddress](_ => ( { + null +}, { case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString) })) -class DirectionSerializer extends CustomSerializer[Direction](format => ({ null },{ +class DirectionSerializer extends CustomSerializer[Direction](_ => ( { + null +}, { case d: Direction => JString(d.toString) })) -class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format => ( { +class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( { null }, { - case p: PaymentRequest => { + case p: PaymentRequest => val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq - val fieldList = List(JField("prefix", JString(p.prefix)), JField("timestamp", JLong(p.timestamp)), JField("nodeId", JString(p.nodeId.toString())), JField("serialized", JString(PaymentRequest.write(p))), JField("description", JString(p.description match { - case Left(l) => l.toString() + case Left(l) => l case Right(r) => r.toString() })), JField("paymentHash", JString(p.paymentHash.toString()))) ++ expiry ++ minFinalCltvExpiry ++ amount - JObject(fieldList) - } })) -class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { +class JavaUUIDSerializer extends CustomSerializer[UUID](_ => ( { + null +}, { case id: UUID => JString(id.toString) })) +case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { + val reverse: Map[String, Class[_]] = custom.map(_.swap) + + override val hints: List[Class[_]] = custom.keys.toList + + override def hintFor(clazz: Class[_]): String = custom.getOrElse(clazz, { + throw new IllegalArgumentException(s"No type hint mapping found for $clazz") + }) + + override def classFor(hint: String): Option[Class[_]] = reverse.get(hint) +} + +object CustomTypeHints { + val incomingPaymentStatus = CustomTypeHints(Map( + IncomingPaymentStatus.Pending.getClass -> "pending", + IncomingPaymentStatus.Expired.getClass -> "expired", + classOf[IncomingPaymentStatus.Received] -> "received" + )) + + val outgoingPaymentStatus = CustomTypeHints(Map( + OutgoingPaymentStatus.Pending.getClass -> "pending", + classOf[OutgoingPaymentStatus.Failed] -> "failed", + classOf[OutgoingPaymentStatus.Succeeded] -> "sent" + )) + + val paymentEvent = CustomTypeHints(Map( + classOf[PaymentSent] -> "payment-sent", + classOf[PaymentRelayed] -> "payment-relayed", + classOf[PaymentReceived] -> "payment-received", + classOf[PaymentSettlingOnChain] -> "payment-settling-onchain", + classOf[PaymentFailed] -> "payment-failed" + )) +} + object JsonSupport extends Json4sSupport { implicit val serialization = jackson.Serialization - implicit val formats = org.json4s.DefaultFormats + + implicit val formats = (org.json4s.DefaultFormats + new ByteVectorSerializer + new ByteVector32Serializer + new ByteVector64Serializer + @@ -216,17 +302,9 @@ object JsonSupport extends Json4sSupport { new NodeAddressSerializer + new DirectionSerializer + new PaymentRequestSerializer + - new JavaUUIDSerializer - - case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { - val reverse: Map[String, Class[_]] = custom.map(_.swap) - - override val hints: List[Class[_]] = custom.keys.toList - override def hintFor(clazz: Class[_]): String = custom.getOrElse(clazz, { - throw new IllegalArgumentException(s"No type hint mapping found for $clazz") - }) - override def classFor(hint: String): Option[Class[_]] = reverse.get(hint) - } - + new JavaUUIDSerializer + + CustomTypeHints.incomingPaymentStatus + + CustomTypeHints.outgoingPaymentStatus + + CustomTypeHints.paymentEvent).withTypeHintFieldName("type") } \ 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 23feee8fe6..5a84012d30 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 @@ -34,9 +34,8 @@ import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.api.FormParamExtractors._ -import fr.acinq.eclair.api.JsonSupport.CustomTypeHints import fr.acinq.eclair.io.NodeURI -import fr.acinq.eclair.payment.{PaymentFailed, PaymentReceived, PaymentRequest, _} +import fr.acinq.eclair.payment.{PaymentEvent, PaymentRequest} import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi} import grizzled.slf4j.Logging import scodec.bits.ByteVector @@ -51,16 +50,6 @@ trait Service extends ExtraDirectives with Logging { // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 import JsonSupport.{formats, marshaller, serialization} - // used to send typed messages over the websocket - val formatsWithTypeHint = formats.withTypeHintFieldName("type") + - CustomTypeHints(Map( - classOf[PaymentSent] -> "payment-sent", - classOf[PaymentRelayed] -> "payment-relayed", - classOf[PaymentReceived] -> "payment-received", - classOf[PaymentSettlingOnChain] -> "payment-settling-onchain", - classOf[PaymentFailed] -> "payment-failed" - )) - def password: String val eclairApi: Eclair @@ -99,13 +88,11 @@ trait Service extends ExtraDirectives with Logging { actorSystem.actorOf(Props(new Actor { override def preStart: Unit = { - context.system.eventStream.subscribe(self, classOf[PaymentFailed]) context.system.eventStream.subscribe(self, classOf[PaymentEvent]) } def receive: Receive = { - case message: PaymentFailed => flowInput.offer(serialization.write(message)(formatsWithTypeHint)) - case message: PaymentEvent => flowInput.offer(serialization.write(message)(formatsWithTypeHint)) + case message: PaymentEvent => flowInput.offer(serialization.write(message)) } })) diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired new file mode 100644 index 0000000000..5e8692b1c2 --- /dev/null +++ b/eclair-node/src/test/resources/api/received-expired @@ -0,0 +1 @@ +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"expired"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending new file mode 100644 index 0000000000..b12ccaf6b3 --- /dev/null +++ b/eclair-node/src/test/resources/api/received-pending @@ -0,0 +1 @@ +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"pending"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success new file mode 100644 index 0000000000..38a33dc419 --- /dev/null +++ b/eclair-node/src/test/resources/api/received-success @@ -0,0 +1 @@ +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-failed b/eclair-node/src/test/resources/api/sent-failed new file mode 100644 index 0000000000..4e1883146c --- /dev/null +++ b/eclair-node/src/test/resources/api/sent-failed @@ -0,0 +1 @@ +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"failed","failures":[],"completedAt":2}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-pending b/eclair-node/src/test/resources/api/sent-pending new file mode 100644 index 0000000000..27cab46d7b --- /dev/null +++ b/eclair-node/src/test/resources/api/sent-pending @@ -0,0 +1 @@ +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"pending"}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-success b/eclair-node/src/test/resources/api/sent-success new file mode 100644 index 0000000000..aeaa2b573e --- /dev/null +++ b/eclair-node/src/test/resources/api/sent-success @@ -0,0 +1 @@ +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":3}}] \ No newline at end of file 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 83fcc74f0b..a4eba2e96c 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 @@ -30,6 +30,7 @@ import de.heikoseeberger.akkahttpjson4s.Json4sSupport import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair._ +import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.io.Peer.PeerInfo import fr.acinq.eclair.payment.{PaymentFailed, _} @@ -292,12 +293,21 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock } } - test("'getreceivedinfo' method should respond HTTP 404 with a JSON encoded response if the element is not found") { + test("'getreceivedinfo'") { + val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" + val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, 42, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] - eclair.receivedInfo(any[ByteVector32])(any) returns Future.successful(None) + val notFound = randomBytes32 + eclair.receivedInfo(notFound)(any) returns Future.successful(None) + val pending = randomBytes32 + eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment)) + val expired = randomBytes32 + eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired))) + val received = randomBytes32 + eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, 45)))) val mockService = new MockService(eclair) - Post("/getreceivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> + Post("/getreceivedinfo", FormData("paymentHash" -> notFound.toHex).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> check { @@ -305,7 +315,85 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock assert(status == NotFound) val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) assert(resp == ErrorResponse("Not found")) - eclair.receivedInfo(ByteVector32.Zeroes)(any[Timeout]).wasCalled(once) + eclair.receivedInfo(notFound)(any[Timeout]).wasCalled(once) + } + + Post("/getreceivedinfo", FormData("paymentHash" -> pending.toHex).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("received-pending", response) + eclair.receivedInfo(pending)(any[Timeout]).wasCalled(once) + } + + Post("/getreceivedinfo", FormData("paymentHash" -> expired.toHex).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("received-expired", response) + eclair.receivedInfo(expired)(any[Timeout]).wasCalled(once) + } + + Post("/getreceivedinfo", FormData("paymentHash" -> received.toHex).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("received-success", response) + eclair.receivedInfo(received)(any[Timeout]).wasCalled(once) + } + } + + test("'getsentinfo'") { + val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, 42 msat, aliceNodeId, 1, None, OutgoingPaymentStatus.Pending) + val eclair = mock[Eclair] + val pending = UUID.randomUUID() + eclair.sentInfo(Left(pending))(any) returns Future.successful(Seq(defaultPayment)) + val failed = UUID.randomUUID() + eclair.sentInfo(Left(failed))(any) returns Future.successful(Seq(defaultPayment.copy(status = OutgoingPaymentStatus.Failed(Nil, 2)))) + val sent = UUID.randomUUID() + eclair.sentInfo(Left(sent))(any) returns Future.successful(Seq(defaultPayment.copy(status = OutgoingPaymentStatus.Succeeded(ByteVector32.One, 5 msat, Nil, 3)))) + val mockService = new MockService(eclair) + + Post("/getsentinfo", FormData("id" -> pending.toString).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("sent-pending", response) + eclair.sentInfo(Left(pending))(any[Timeout]).wasCalled(once) + } + + Post("/getsentinfo", FormData("id" -> failed.toString).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("sent-failed", response) + eclair.sentInfo(Left(failed))(any[Timeout]).wasCalled(once) + } + + Post("/getsentinfo", FormData("id" -> sent.toString).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("sent-success", response) + eclair.sentInfo(Left(sent))(any[Timeout]).wasCalled(once) } } @@ -348,45 +436,42 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock test("the websocket should return typed objects") { val mockService = new MockService(mock[Eclair]) val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") - val wsClient = WSProbe() WS("/ws", wsClient.flow) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> mockService.route ~> check { - val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L) val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":1553784963659}""" - serialization.write(pf)(mockService.formatsWithTypeHint) === expectedSerializedPf + assert(serialization.write(pf) === expectedSerializedPf) system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, 1553784337711L))) val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}]}""" - serialization.write(ps)(mockService.formatsWithTypeHint) === expectedSerializedPs + assert(serialization.write(ps) === expectedSerializedPs) system.eventStream.publish(ps) wsClient.expectMessage(expectedSerializedPs) val prel = PaymentRelayed(amountIn = 21 msat, amountOut = 20 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L) val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" - serialization.write(prel)(mockService.formatsWithTypeHint) === expectedSerializedPrel + assert(serialization.write(prel) === expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, 1553784963659L))) val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}]}""" - serialization.write(precv)(mockService.formatsWithTypeHint) === expectedSerializedPrecv + assert(serialization.write(precv) === expectedSerializedPrecv) system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) val pset = PaymentSettlingOnChain(fixedUUID, amount = 21 msat, paymentHash = ByteVector32.One, timestamp = 1553785442676L) val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553785442676}""" - serialization.write(pset)(mockService.formatsWithTypeHint) === expectedSerializedPset + assert(serialization.write(pset) === expectedSerializedPset) system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset) } - } private def matchTestJson(apiName: String, response: String) = { diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index da2169739a..6d1e811f12 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -21,11 +21,9 @@ import java.util.UUID import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction} import fr.acinq.eclair._ -import fr.acinq.eclair.api.JsonSupport.CustomTypeHints import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} -import org.json4s.jackson.Serialization import org.scalatest.{FunSuite, Matchers} import scodec.bits._ @@ -34,8 +32,6 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("deserialize Map[OutPoint, ByteVector]") { val output1 = OutPoint(ByteVector32(hex"11418a2d282a40461966e4f578e1fdf633ad15c1b7fb3e771d14361127233be1"), 0) val output2 = OutPoint(ByteVector32(hex"3d62bd4f71dc63798418e59efbc7532380c900b5e79db3a5521374b161dd0e33"), 1) - - val map = Map( output1 -> hex"dead", output2 -> hex"beef" @@ -43,12 +39,12 @@ class JsonSerializersSpec extends FunSuite with Matchers { // it won't work with the default key serializer val error = intercept[org.json4s.MappingException] { - Serialization.write(map)(org.json4s.DefaultFormats) + JsonSupport.serialization.write(map)(org.json4s.DefaultFormats) } assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.OutPoint.")) // but it works with our custom key serializer - val json = Serialization.write(map)(org.json4s.DefaultFormats + new ByteVectorSerializer + new OutPointKeySerializer) + val json = JsonSupport.serialization.write(map)(org.json4s.DefaultFormats + new ByteVectorSerializer + new OutPointKeySerializer) assert(json === s"""{"${output1.txid}:0":"dead","${output2.txid}:1":"beef"}""") } @@ -58,15 +54,15 @@ class JsonSerializersSpec extends FunSuite with Matchers { val tor2 = Tor2("aaaqeayeaudaocaj", 7777) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) - Serialization.write(ipv4)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" - Serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" - Serialization.write(tor2)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" - Serialization.write(tor3)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" + JsonSupport.serialization.write(ipv4)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" + JsonSupport.serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" + JsonSupport.serialization.write(tor2)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" + JsonSupport.serialization.write(tor3)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" } test("Direction serialization") { - Serialization.write(IN)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""IN"""" - Serialization.write(OUT)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""OUT"""" + JsonSupport.serialization.write(IN)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""IN"""" + JsonSupport.serialization.write(OUT)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""OUT"""" } test("Payment Request") { @@ -76,14 +72,12 @@ class JsonSerializersSpec extends FunSuite with Matchers { } test("type hints") { - implicit val formats = JsonSupport.formats.withTypeHintFieldName("type") + CustomTypeHints(Map(classOf[PaymentSettlingOnChain] -> "payment-settling-onchain")) + new MilliSatoshiSerializer val e1 = PaymentSettlingOnChain(UUID.randomUUID, 42 msat, randomBytes32) - assert(Serialization.writePretty(e1).contains("\"type\" : \"payment-settling-onchain\"")) + assert(JsonSupport.serialization.writePretty(e1)(JsonSupport.formats).contains("\"type\" : \"payment-settling-onchain\"")) } test("transaction serializer") { - implicit val formats = JsonSupport.formats val tx = Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800") - assert(JsonSupport.serialization.write(tx) == "{\"txid\":\"3ef63b5d297c9dcf93f33b45b9f102733c36e8ef61da1ccf2bc132a10584be18\",\"tx\":\"0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800\"}") + assert(JsonSupport.serialization.write(tx)(JsonSupport.formats) == "{\"txid\":\"3ef63b5d297c9dcf93f33b45b9f102733c36e8ef61da1ccf2bc132a10584be18\",\"tx\":\"0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800\"}") } } \ No newline at end of file