1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 14:22:39 +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.bitcoin.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction}
import fr.acinq.eclair.channel.{ChannelVersion, State} import fr.acinq.eclair.channel.{ChannelVersion, State}
import fr.acinq.eclair.crypto.ShaChain 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.router.RouteResponse
import fr.acinq.eclair.transactions.Direction import fr.acinq.eclair.transactions.Direction
import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo}
@ -39,97 +40,141 @@ import scodec.bits.ByteVector
* JSON Serializers. * JSON Serializers.
* Note: in general, deserialization does not need to be implemented. * Note: in general, deserialization does not need to be implemented.
*/ */
class ByteVectorSerializer extends CustomSerializer[ByteVector](format => ({ null }, { class ByteVectorSerializer extends CustomSerializer[ByteVector](_ => ( {
null
}, {
case x: ByteVector => JString(x.toHex) 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) 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) 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) 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) 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) 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) 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) case x: CltvExpiryDelta => JInt(x.toInt)
})) }))
class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ({ null }, { class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](_ => ( {
case x: ShortChannelId => JString(x.toString()) null
}, {
case x: ShortChannelId => JString(x.toString)
})) }))
class StateSerializer extends CustomSerializer[State](format => ({ null }, { class StateSerializer extends CustomSerializer[State](_ => ( {
case x: State => JString(x.toString()) null
}, {
case x: State => JString(x.toString)
})) }))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ({ null }, { class ShaChainSerializer extends CustomSerializer[ShaChain](_ => ( {
case x: ShaChain => JNull null
}, {
case _: ShaChain => JNull
})) }))
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ({ null }, { class PublicKeySerializer extends CustomSerializer[PublicKey](_ => ( {
null
}, {
case x: PublicKey => JString(x.toString()) case x: PublicKey => JString(x.toString())
})) }))
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ({ null }, { class PrivateKeySerializer extends CustomSerializer[PrivateKey](_ => ( {
case x: PrivateKey => JString("XXX") 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) 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( case x: Transaction => JObject(List(
JField("txid", JString(x.txid.toHex)), JField("txid", JString(x.txid.toHex)),
JField("tx", JString(x.toString())) JField("tx", JString(x.toString()))
)) ))
})) }))
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ({ null }, { class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( {
null
}, {
case x: TransactionWithInputInfo => JObject(List( case x: TransactionWithInputInfo => JObject(List(
JField("txid", JString(x.tx.txid.toHex)), JField("txid", JString(x.tx.txid.toHex)),
JField("tx", JString(x.tx.toString())) 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) 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}") 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}" 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))) 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) case c: Color => JString(c.toString)
})) }))
class RouteResponseSerializer extends CustomSerializer[RouteResponse](format => ({ null }, { class RouteResponseSerializer extends CustomSerializer[RouteResponse](_ => ( {
null
}, {
case route: RouteResponse => case route: RouteResponse =>
val nodeIds = route.hops match { val nodeIds = route.hops match {
case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId 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))) 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 if t.getMessage != null => JString(t.getMessage)
case t: Throwable => JString(t.getClass.getSimpleName) 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) 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) 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) case d: Direction => JString(d.toString)
})) }))
class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format => ( { class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( {
null null
}, { }, {
case p: PaymentRequest => { case p: PaymentRequest =>
val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq
val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).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 amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq
val fieldList = List(JField("prefix", JString(p.prefix)), val fieldList = List(JField("prefix", JString(p.prefix)),
JField("timestamp", JLong(p.timestamp)), JField("timestamp", JLong(p.timestamp)),
JField("nodeId", JString(p.nodeId.toString())), JField("nodeId", JString(p.nodeId.toString())),
JField("serialized", JString(PaymentRequest.write(p))), JField("serialized", JString(PaymentRequest.write(p))),
JField("description", JString(p.description match { JField("description", JString(p.description match {
case Left(l) => l.toString() case Left(l) => l
case Right(r) => r.toString() case Right(r) => r.toString()
})), })),
JField("paymentHash", JString(p.paymentHash.toString()))) ++ JField("paymentHash", JString(p.paymentHash.toString()))) ++
expiry ++ expiry ++
minFinalCltvExpiry ++ minFinalCltvExpiry ++
amount amount
JObject(fieldList) JObject(fieldList)
}
})) }))
class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { class JavaUUIDSerializer extends CustomSerializer[UUID](_ => ( {
null
}, {
case id: UUID => JString(id.toString) 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 { object JsonSupport extends Json4sSupport {
implicit val serialization = jackson.Serialization implicit val serialization = jackson.Serialization
implicit val formats = org.json4s.DefaultFormats + implicit val formats = (org.json4s.DefaultFormats +
new ByteVectorSerializer + new ByteVectorSerializer +
new ByteVector32Serializer + new ByteVector32Serializer +
new ByteVector64Serializer + new ByteVector64Serializer +
@ -216,17 +302,9 @@ object JsonSupport extends Json4sSupport {
new NodeAddressSerializer + new NodeAddressSerializer +
new DirectionSerializer + new DirectionSerializer +
new PaymentRequestSerializer + new PaymentRequestSerializer +
new JavaUUIDSerializer new JavaUUIDSerializer +
CustomTypeHints.incomingPaymentStatus +
case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { CustomTypeHints.outgoingPaymentStatus +
val reverse: Map[String, Class[_]] = custom.map(_.swap) CustomTypeHints.paymentEvent).withTypeHintFieldName("type")
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)
}
} }

View file

@ -34,9 +34,8 @@ import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.api.FormParamExtractors._ import fr.acinq.eclair.api.FormParamExtractors._
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints
import fr.acinq.eclair.io.NodeURI 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 fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import scodec.bits.ByteVector 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 // 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} 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 def password: String
val eclairApi: Eclair val eclairApi: Eclair
@ -99,13 +88,11 @@ trait Service extends ExtraDirectives with Logging {
actorSystem.actorOf(Props(new Actor { actorSystem.actorOf(Props(new Actor {
override def preStart: Unit = { override def preStart: Unit = {
context.system.eventStream.subscribe(self, classOf[PaymentFailed])
context.system.eventStream.subscribe(self, classOf[PaymentEvent]) context.system.eventStream.subscribe(self, classOf[PaymentEvent])
} }
def receive: Receive = { def receive: Receive = {
case message: PaymentFailed => flowInput.offer(serialization.write(message)(formatsWithTypeHint)) case message: PaymentEvent => flowInput.offer(serialization.write(message))
case message: PaymentEvent => flowInput.offer(serialization.write(message)(formatsWithTypeHint))
} }
})) }))

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.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair._ import fr.acinq.eclair._
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.io.NodeURI
import fr.acinq.eclair.io.Peer.PeerInfo import fr.acinq.eclair.io.Peer.PeerInfo
import fr.acinq.eclair.payment.{PaymentFailed, _} 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] 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) val mockService = new MockService(eclair)
Post("/getreceivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> Post("/getreceivedinfo", FormData("paymentHash" -> notFound.toHex).toEntity) ~>
addCredentials(BasicHttpCredentials("", mockService.password)) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~>
Route.seal(mockService.route) ~> Route.seal(mockService.route) ~>
check { check {
@ -305,7 +315,85 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
assert(status == NotFound) assert(status == NotFound)
val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse]))
assert(resp == ErrorResponse("Not found")) 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") { test("the websocket should return typed objects") {
val mockService = new MockService(mock[Eclair]) val mockService = new MockService(mock[Eclair])
val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f")
val wsClient = WSProbe() val wsClient = WSProbe()
WS("/ws", wsClient.flow) ~> WS("/ws", wsClient.flow) ~>
addCredentials(BasicHttpCredentials("", mockService.password)) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~>
mockService.route ~> mockService.route ~>
check { check {
val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L) 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}""" 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) system.eventStream.publish(pf)
wsClient.expectMessage(expectedSerializedPf) wsClient.expectMessage(expectedSerializedPf)
val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, 1553784337711L))) 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}]}""" 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) system.eventStream.publish(ps)
wsClient.expectMessage(expectedSerializedPs) wsClient.expectMessage(expectedSerializedPs)
val prel = PaymentRelayed(amountIn = 21 msat, amountOut = 20 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L) 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}""" 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) system.eventStream.publish(prel)
wsClient.expectMessage(expectedSerializedPrel) wsClient.expectMessage(expectedSerializedPrel)
val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, 1553784963659L))) 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}]}""" 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) system.eventStream.publish(precv)
wsClient.expectMessage(expectedSerializedPrecv) wsClient.expectMessage(expectedSerializedPrecv)
val pset = PaymentSettlingOnChain(fixedUUID, amount = 21 msat, paymentHash = ByteVector32.One, timestamp = 1553785442676L) 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}""" 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) system.eventStream.publish(pset)
wsClient.expectMessage(expectedSerializedPset) wsClient.expectMessage(expectedSerializedPset)
} }
} }
private def matchTestJson(apiName: String, response: String) = { 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.bitcoin.{ByteVector32, OutPoint, Transaction}
import fr.acinq.eclair._ import fr.acinq.eclair._
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints
import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain}
import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.transactions.{IN, OUT}
import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3}
import org.json4s.jackson.Serialization
import org.scalatest.{FunSuite, Matchers} import org.scalatest.{FunSuite, Matchers}
import scodec.bits._ import scodec.bits._
@ -34,8 +32,6 @@ class JsonSerializersSpec extends FunSuite with Matchers {
test("deserialize Map[OutPoint, ByteVector]") { test("deserialize Map[OutPoint, ByteVector]") {
val output1 = OutPoint(ByteVector32(hex"11418a2d282a40461966e4f578e1fdf633ad15c1b7fb3e771d14361127233be1"), 0) val output1 = OutPoint(ByteVector32(hex"11418a2d282a40461966e4f578e1fdf633ad15c1b7fb3e771d14361127233be1"), 0)
val output2 = OutPoint(ByteVector32(hex"3d62bd4f71dc63798418e59efbc7532380c900b5e79db3a5521374b161dd0e33"), 1) val output2 = OutPoint(ByteVector32(hex"3d62bd4f71dc63798418e59efbc7532380c900b5e79db3a5521374b161dd0e33"), 1)
val map = Map( val map = Map(
output1 -> hex"dead", output1 -> hex"dead",
output2 -> hex"beef" output2 -> hex"beef"
@ -43,12 +39,12 @@ class JsonSerializersSpec extends FunSuite with Matchers {
// it won't work with the default key serializer // it won't work with the default key serializer
val error = intercept[org.json4s.MappingException] { 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.")) 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 // 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"}""") 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 tor2 = Tor2("aaaqeayeaudaocaj", 7777)
val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999)
Serialization.write(ipv4)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" JsonSupport.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"""" JsonSupport.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"""" JsonSupport.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(tor3)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999""""
} }
test("Direction serialization") { test("Direction serialization") {
Serialization.write(IN)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""IN"""" JsonSupport.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(OUT)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""OUT""""
} }
test("Payment Request") { test("Payment Request") {
@ -76,14 +72,12 @@ class JsonSerializersSpec extends FunSuite with Matchers {
} }
test("type hints") { 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) 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") { test("transaction serializer") {
implicit val formats = JsonSupport.formats
val tx = Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800") 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\"}")
} }
} }