1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-21 22:11:46 +01:00

HTTP API: add type hints for payment status (#1150)

Cleans up the JSON payment status (easier to interpret for callers).
This commit is contained in:
Bastien Teinturier 2019-10-01 10:16:29 +02:00 committed by GitHub
parent 24d11884fa
commit 7458383ecd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 247 additions and 97 deletions

View file

@ -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")
}

View file

@ -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))
}
}))

View file

@ -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"}}

View file

@ -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"}}

View file

@ -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}}

View file

@ -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}}]

View file

@ -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"}}]

View file

@ -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}}]

View file

@ -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) = {

View file

@ -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\"}")
}
}