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:
parent
24d11884fa
commit
7458383ecd
10 changed files with 247 additions and 97 deletions
|
@ -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")
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}))
|
||||
|
|
1
eclair-node/src/test/resources/api/received-expired
Normal file
1
eclair-node/src/test/resources/api/received-expired
Normal 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"}}
|
1
eclair-node/src/test/resources/api/received-pending
Normal file
1
eclair-node/src/test/resources/api/received-pending
Normal 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"}}
|
1
eclair-node/src/test/resources/api/received-success
Normal file
1
eclair-node/src/test/resources/api/received-success
Normal 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}}
|
1
eclair-node/src/test/resources/api/sent-failed
Normal file
1
eclair-node/src/test/resources/api/sent-failed
Normal 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}}]
|
1
eclair-node/src/test/resources/api/sent-pending
Normal file
1
eclair-node/src/test/resources/api/sent-pending
Normal 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"}}]
|
1
eclair-node/src/test/resources/api/sent-success
Normal file
1
eclair-node/src/test/resources/api/sent-success
Normal 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}}]
|
|
@ -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) = {
|
||||
|
|
|
@ -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\"}")
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue