Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP API: add type hints for payment status #1150

Merged
merged 3 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 132 additions & 54 deletions eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand All @@ -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 +
Expand Down Expand Up @@ -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")

}
17 changes: 2 additions & 15 deletions eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
}

}))
Expand Down
1 change: 1 addition & 0 deletions eclair-node/src/test/resources/api/received-expired
Original file line number Diff line number Diff line change
@@ -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"}}
1 change: 1 addition & 0 deletions eclair-node/src/test/resources/api/received-pending
Original file line number Diff line number Diff line change
@@ -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"}}
1 change: 1 addition & 0 deletions eclair-node/src/test/resources/api/received-success
Original file line number Diff line number Diff line change
@@ -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}}
1 change: 1 addition & 0 deletions eclair-node/src/test/resources/api/sent-failed
Original file line number Diff line number Diff line change
@@ -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}}]
1 change: 1 addition & 0 deletions eclair-node/src/test/resources/api/sent-pending
Original file line number Diff line number Diff line change
@@ -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"}}]
1 change: 1 addition & 0 deletions eclair-node/src/test/resources/api/sent-success
Original file line number Diff line number Diff line change
@@ -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}}]
Loading