1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-04 09:58:02 +01:00

Move http APIs to subproject eclair-node (#1102)

* Move Service and FormParamExtractor to eclair-node

* Move dependency akka-http-json4s into eclair-node

* Move json serializers to eclair-node
This commit is contained in:
araspitzu 2019-08-30 09:44:24 +02:00 committed by GitHub
parent d67ba48fc0
commit 74af0304bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 263 additions and 113 deletions

View file

@ -126,12 +126,6 @@
<artifactId>akka-slf4j_${scala.version.short}</artifactId> <artifactId>akka-slf4j_${scala.version.short}</artifactId>
<version>${akka.version}</version> <version>${akka.version}</version>
</dependency> </dependency>
<!-- HTTP SERVER -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-core_${scala.version.short}</artifactId>
<version>${akka.http.version}</version>
</dependency>
<!-- HTTP CLIENT --> <!-- HTTP CLIENT -->
<dependency> <dependency>
<groupId>com.softwaremill.sttp</groupId> <groupId>com.softwaremill.sttp</groupId>
@ -144,11 +138,6 @@
<artifactId>json4s-jackson_${scala.version.short}</artifactId> <artifactId>json4s-jackson_${scala.version.short}</artifactId>
<version>3.6.0</version> <version>3.6.0</version>
</dependency> </dependency>
<dependency>
<groupId>de.heikoseeberger</groupId>
<artifactId>akka-http-json4s_${scala.version.short}</artifactId>
<version>1.19.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.softwaremill.sttp</groupId> <groupId>com.softwaremill.sttp</groupId>
<artifactId>json4s_${scala.version.short}</artifactId> <artifactId>json4s_${scala.version.short}</artifactId>
@ -254,12 +243,6 @@
<version>1.2.3</version> <version>1.2.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-testkit_${scala.version.short}</artifactId>
<version>${akka.http.version}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
<artifactId>mockito-scala-scalatest_2.11</artifactId> <artifactId>mockito-scala-scalatest_2.11</artifactId>

View file

@ -23,15 +23,12 @@ import java.util.concurrent.TimeUnit
import akka.Done import akka.Done
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy} import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
import akka.http.scaladsl.Http
import akka.pattern.after import akka.pattern.after
import akka.stream.{ActorMaterializer, BindFailedException}
import akka.util.Timeout import akka.util.Timeout
import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.softwaremill.sttp.okhttp.OkHttpFutureBackend
import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.bitcoin.{Block, ByteVector32}
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
import fr.acinq.eclair.api._
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher} import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
@ -71,7 +68,6 @@ class Setup(datadir: File,
seed_opt: Option[ByteVector] = None, seed_opt: Option[ByteVector] = None,
db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging { db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging {
implicit val materializer = ActorMaterializer()
implicit val timeout = Timeout(30 seconds) implicit val timeout = Timeout(30 seconds)
implicit val formats = org.json4s.DefaultFormats implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global implicit val ec = ExecutionContext.Implicits.global
@ -287,32 +283,6 @@ class Setup(datadir: File,
_ <- Future.firstCompletedOf(zmqBlockConnected.future :: zmqBlockTimeout :: Nil) _ <- Future.firstCompletedOf(zmqBlockConnected.future :: zmqBlockTimeout :: Nil)
_ <- Future.firstCompletedOf(zmqTxConnected.future :: zmqTxTimeout :: Nil) _ <- Future.firstCompletedOf(zmqTxConnected.future :: zmqTxTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil) _ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
_ <- if (config.getBoolean("api.enabled")) {
logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}")
implicit val materializer = ActorMaterializer()
val getInfo = GetInfoResponse(nodeId = nodeParams.nodeId,
alias = nodeParams.alias,
chainHash = nodeParams.chainHash,
blockHeight = Globals.blockCount.intValue(),
publicAddresses = nodeParams.publicAddresses)
val apiPassword = config.getString("api.password") match {
case "" => throw EmptyAPIPasswordException
case valid => valid
}
val apiRoute = new Service {
override val actorSystem = kit.system
override val mat = materializer
override val password = apiPassword
override val eclairApi: Eclair = new EclairImpl(kit)
}.route
val httpBound = Http().bindAndHandle(apiRoute, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
}
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
} else {
Future.successful(logger.info("json-rpc api is disabled"))
}
} yield kit } yield kit
} }

View file

@ -20,7 +20,6 @@ import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction}
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
import fr.acinq.eclair.api.MilliSatoshiSerializer
import fr.acinq.eclair.channel.Channel.ChannelError import fr.acinq.eclair.channel.Channel.ChannelError
import fr.acinq.eclair.channel.Helpers.Closing.ClosingType import fr.acinq.eclair.channel.Helpers.Closing.ClosingType
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate}

View file

@ -16,22 +16,25 @@
package fr.acinq.eclair.wire package fr.acinq.eclair.wire
import java.net.InetSocketAddress
import java.util.UUID import java.util.UUID
import akka.actor.ActorSystem import akka.actor.ActorSystem
import fr.acinq.bitcoin.Crypto.PrivateKey import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Transaction}
import fr.acinq.eclair.api.JsonSupport
import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain} import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain}
import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.payment.{Local, Relayed}
import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo, TransactionWithInputInfo}
import fr.acinq.eclair.transactions._ import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.ChannelCodecs._ import fr.acinq.eclair.wire.ChannelCodecs._
import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey, _} import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey, _}
import org.json4s.{CustomKeySerializer, CustomSerializer}
import org.json4s.JsonAST._
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization
import org.scalatest.FunSuite import org.scalatest.FunSuite
import scodec.bits._ import scodec.bits._
@ -43,8 +46,8 @@ import scala.io.Source
import scala.util.Random import scala.util.Random
/** /**
* Created by PM on 31/05/2016. * Created by PM on 31/05/2016.
*/ */
class ChannelCodecsSpec extends FunSuite { class ChannelCodecsSpec extends FunSuite {
@ -333,9 +336,7 @@ class ChannelCodecsSpec extends FunSuite {
assert(oldjson === refjson) assert(oldjson === refjson)
assert(newjson === refjson) assert(newjson === refjson)
} }
} }
} }
object ChannelCodecsSpec { object ChannelCodecsSpec {
@ -402,4 +403,155 @@ object ChannelCodecsSpec {
val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), CltvExpiryDelta(42), 15 msat, 575 msat, 53, Channel.MAX_FUNDING.toMilliSatoshi) val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), CltvExpiryDelta(42), 15 msat, 575 msat, 53, Channel.MAX_FUNDING.toMilliSatoshi)
val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None) val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None)
}
object JsonSupport {
class ByteVectorSerializer extends CustomSerializer[ByteVector](format => ( {
null
}, {
case x: ByteVector => JString(x.toHex)
}))
class ByteVector32Serializer extends CustomSerializer[ByteVector32](format => ( {
null
}, {
case x: ByteVector32 => JString(x.toHex)
}))
class ByteVector64Serializer extends CustomSerializer[ByteVector64](format => ( {
null
}, {
case x: ByteVector64 => JString(x.toHex)
}))
class UInt64Serializer extends CustomSerializer[UInt64](format => ( {
null
}, {
case x: UInt64 => JInt(x.toBigInt)
}))
class SatoshiSerializer extends CustomSerializer[Satoshi](format => ( {
null
}, {
case x: Satoshi => JInt(x.toLong)
}))
class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ( {
null
}, {
case x: MilliSatoshi => JInt(x.toLong)
}))
class CltvExpirySerializer extends CustomSerializer[CltvExpiry](format => ( {
null
}, {
case x: CltvExpiry => JLong(x.toLong)
}))
class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](format => ( {
null
}, {
case x: CltvExpiryDelta => JInt(x.toInt)
}))
class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ( {
null
}, {
case x: ShortChannelId => JString(x.toString())
}))
class StateSerializer extends CustomSerializer[State](format => ( {
null
}, {
case x: State => JString(x.toString())
}))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
null
}, {
case x: ShaChain => JNull
}))
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
null
}, {
case x: PublicKey => JString(x.toString())
}))
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
null
}, {
case x: PrivateKey => JString("XXX")
}))
class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](format => ( {
null
}, {
case x: ChannelVersion => JString(x.bits.toBin)
}))
class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ( {
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
}, {
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
}, {
case address: InetSocketAddress => JString(HostAndPort.fromParts(address.getHostString, address.getPort).toString)
}))
class OutPointSerializer extends CustomSerializer[OutPoint](format => ( {
null
}, {
case x: OutPoint => JString(s"${x.txid}:${x.index}")
}))
class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( {
null
}, {
case x: OutPoint => s"${x.txid}:${x.index}"
}))
class InputInfoSerializer extends CustomSerializer[InputInfo](format => ( {
null
}, {
case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.toLong)))
}))
implicit val formats = org.json4s.DefaultFormats +
new ByteVectorSerializer +
new ByteVector32Serializer +
new ByteVector64Serializer +
new UInt64Serializer +
new SatoshiSerializer +
new MilliSatoshiSerializer +
new CltvExpirySerializer +
new CltvExpiryDeltaSerializer +
new ShortChannelIdSerializer +
new StateSerializer +
new ShaChainSerializer +
new PublicKeySerializer +
new PrivateKeySerializer +
new TransactionSerializer +
new TransactionWithInputInfoSerializer +
new InetSocketAddressSerializer +
new OutPointSerializer +
new OutPointKeySerializer +
new ChannelVersionSerializer +
new InputInfoSerializer
}
}

View file

@ -123,11 +123,6 @@ class LightningMessageCodecsSpec extends FunSuite {
case class TestItem(msg: Any, hex: String) case class TestItem(msg: Any, hex: String)
test("test vectors for extended channel queries ") { test("test vectors for extended channel queries ") {
import org.json4s.{CustomSerializer, ShortTypeHints}
import org.json4s.JsonAST.JString
import org.json4s.jackson.Serialization
import fr.acinq.eclair.api._
val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, TlvStream.empty) val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, TlvStream.empty)
val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId,
35000, 35000,

View file

@ -22,7 +22,7 @@ import akka.actor.ActorSystem
import fr.acinq.eclair.gui.{FxApp, FxPreloader} import fr.acinq.eclair.gui.{FxApp, FxPreloader}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import javafx.application.Application import javafx.application.Application
import scala.concurrent.ExecutionContext.Implicits.global
/** /**
* Created by PM on 25/01/2016. * Created by PM on 25/01/2016.
*/ */
@ -33,7 +33,10 @@ object JavafxBoot extends App with Logging {
if (headless) { if (headless) {
implicit val system = ActorSystem("eclair-node-gui") implicit val system = ActorSystem("eclair-node-gui")
new Setup(datadir).bootstrap val setup = new Setup(datadir)
setup.bootstrap.map { kit =>
Boot.startApiServiceIfEnabled(setup.config, kit)
}
} else { } else {
System.setProperty("javafx.preloader", classOf[FxPreloader].getName) System.setProperty("javafx.preloader", classOf[FxPreloader].getName)
Application.launch(classOf[FxApp], datadir.getAbsolutePath) Application.launch(classOf[FxApp], datadir.getAbsolutePath)

View file

@ -104,7 +104,8 @@ class FxApp extends Application with Logging {
system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent]) system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
pKit.completeWith(setup.bootstrap) pKit.completeWith(setup.bootstrap)
pKit.future.onComplete { pKit.future.onComplete {
case Success(_) => case Success(kit) =>
Boot.startApiServiceIfEnabled(setup.config, kit)
Platform.runLater(new Runnable { Platform.runLater(new Runnable {
override def run(): Unit = { override def run(): Unit = {
val scene = new Scene(mainRoot) val scene = new Scene(mainRoot)

View file

@ -91,5 +91,29 @@
<artifactId>janino</artifactId> <artifactId>janino</artifactId>
<version>3.0.7</version> <version>3.0.7</version>
</dependency> </dependency>
<!-- http server -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-core_${scala.version.short}</artifactId>
<version>${akka.http.version}</version>
</dependency>
<dependency>
<groupId>de.heikoseeberger</groupId>
<artifactId>akka-http-json4s_${scala.version.short}</artifactId>
<version>1.19.0</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-testkit_${scala.version.short}</artifactId>
<version>${akka.http.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-scala-scalatest_2.11</artifactId>
<version>1.4.1</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -19,9 +19,12 @@ package fr.acinq.eclair
import java.io.File import java.io.File
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.{ActorMaterializer, BindFailedException}
import com.typesafe.config.Config
import fr.acinq.eclair.api.Service
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import scala.concurrent.{Await, ExecutionContext}
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
/** /**
@ -39,13 +42,45 @@ object Boot extends App with Logging {
val setup = new Setup(datadir) val setup = new Setup(datadir)
plugins.foreach(_.onSetup(setup)) plugins.foreach(_.onSetup(setup))
setup.bootstrap onComplete { setup.bootstrap onComplete {
case Success(kit) => plugins.foreach(_.onKit(kit)) case Success(kit) =>
startApiServiceIfEnabled(setup.config, kit)
plugins.foreach(_.onKit(kit))
case Failure(t) => onError(t) case Failure(t) => onError(t)
} }
} catch { } catch {
case t: Throwable => onError(t) case t: Throwable => onError(t)
} }
/**
* Starts the http APIs service if enabled in the configuration
*
* @param config
* @param kit
* @param system
* @param ec
*/
def startApiServiceIfEnabled(config: Config, kit: Kit)(implicit system: ActorSystem, ec: ExecutionContext) = {
if(config.getBoolean("api.enabled")){
logger.info(s"json API enabled on port=${config.getInt("api.port")}")
implicit val materializer = ActorMaterializer()
val apiPassword = config.getString("api.password") match {
case "" => throw EmptyAPIPasswordException
case valid => valid
}
val apiRoute = new Service {
override val actorSystem = system
override val mat = materializer
override val password = apiPassword
override val eclairApi: Eclair = new EclairImpl(kit)
}.route
Http().bindAndHandle(apiRoute, config.getString("api.binding-ip"), config.getInt("api.port")).onFailure {
case _: BindFailedException => onError(TCPBindException(config.getInt("api.port")))
}
} else {
logger.info("json API disabled")
}
}
def onError(t: Throwable): Unit = { def onError(t: Throwable): Unit = {
val errorMsg = if (t.getMessage != null) t.getMessage else t.getClass.getSimpleName val errorMsg = if (t.getMessage != null) t.getMessage else t.getClass.getSimpleName
System.err.println(s"fatal error: $errorMsg") System.err.println(s"fatal error: $errorMsg")

View file

@ -26,7 +26,6 @@ import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
import fr.acinq.eclair.api.FormParamExtractors.{sha256HashUnmarshaller, shortChannelIdUnmarshaller} import fr.acinq.eclair.api.FormParamExtractors.{sha256HashUnmarshaller, shortChannelIdUnmarshaller}
import fr.acinq.eclair.api.JsonSupport._ import fr.acinq.eclair.api.JsonSupport._
import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.payment.PaymentRequest
import scala.concurrent.Future import scala.concurrent.Future
import scala.util.{Failure, Success} import scala.util.{Failure, Success}

View file

@ -18,16 +18,15 @@ package fr.acinq.eclair.api
import java.util.UUID import java.util.UUID
import JsonSupport._
import akka.http.scaladsl.unmarshalling.Unmarshaller import akka.http.scaladsl.unmarshalling.Unmarshaller
import akka.util.Timeout import akka.util.Timeout
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.api.JsonSupport._
import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.io.NodeURI
import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.payment.PaymentRequest
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}

View file

@ -21,7 +21,6 @@ import java.util.UUID
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import de.heikoseeberger.akkahttpjson4s.Json4sSupport import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} 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}
@ -225,8 +224,6 @@ object JsonSupport extends Json4sSupport {
new JavaUUIDSerializer + new JavaUUIDSerializer +
new OutgoingPaymentStatusSerializer new OutgoingPaymentStatusSerializer
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints {
val reverse: Map[String, Class[_]] = custom.map(_.swap) val reverse: Map[String, Class[_]] = custom.map(_.swap)

View file

@ -31,6 +31,7 @@ import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source}
import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.{ActorMaterializer, OverflowStrategy}
import akka.util.Timeout import akka.util.Timeout
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
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._
@ -40,7 +41,6 @@ import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRequest, _} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRequest, _}
import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi} import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.json4s.jackson.Serialization
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.concurrent.Future import scala.concurrent.Future
@ -103,8 +103,8 @@ trait Service extends ExtraDirectives with Logging {
} }
def receive: Receive = { def receive: Receive = {
case message: PaymentFailed => flowInput.offer(Serialization.write(message)(formatsWithTypeHint)) 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)(formatsWithTypeHint))
} }
})) }))

View file

@ -25,22 +25,20 @@ import akka.http.scaladsl.model.headers.BasicHttpCredentials
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe}
import akka.stream.ActorMaterializer import akka.stream.ActorMaterializer
import akka.util.Timeout import akka.util.{ByteString, Timeout}
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
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.TestConstants._
import fr.acinq.eclair._ import fr.acinq.eclair._
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.PaymentLifecycle.PaymentFailed import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.wire.NodeAddress import fr.acinq.eclair.wire.NodeAddress
import org.json4s.jackson.Serialization
import org.mockito.scalatest.IdiomaticMockito import org.mockito.scalatest.IdiomaticMockito
import org.scalatest.{FunSuite, Matchers} import org.scalatest.{FunSuite, Matchers}
import scodec.bits._ import scodec.bits._
import scala.concurrent.{Await, Future}
import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.io.Source import scala.io.Source
import scala.reflect.ClassTag import scala.reflect.ClassTag
@ -50,11 +48,11 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
implicit val formats = JsonSupport.formats implicit val formats = JsonSupport.formats
implicit val serialization = JsonSupport.serialization implicit val serialization = JsonSupport.serialization
implicit val marshaller = JsonSupport.marshaller
implicit val unmarshaller = JsonSupport.unmarshaller
implicit val routeTestTimeout = RouteTestTimeout(3 seconds) implicit val routeTestTimeout = RouteTestTimeout(3 seconds)
val aliceNodeId = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0")
val bobNodeId = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585")
class MockService(eclair: Eclair) extends Service { class MockService(eclair: Eclair) extends Service {
override val eclairApi: Eclair = eclair override val eclairApi: Eclair = eclair
@ -100,7 +98,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
check { check {
assert(handled) assert(handled)
assert(status == BadRequest) assert(status == BadRequest)
val resp = entityAs[ErrorResponse](JsonSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse]))
assert(resp.error == "The form field 'channelId' was malformed:\nInvalid hexadecimal character 'h' at index 0") assert(resp.error == "The form field 'channelId' was malformed:\nInvalid hexadecimal character 'h' at index 0")
} }
@ -112,7 +110,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
assert(handled) assert(handled)
assert(status == BadRequest) assert(status == BadRequest)
} }
} }
test("'peers' should ask the switchboard for current known peers") { test("'peers' should ask the switchboard for current known peers") {
@ -121,12 +118,12 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val mockService = new MockService(eclair) val mockService = new MockService(eclair)
eclair.peersInfo()(any[Timeout]) returns Future.successful(List( eclair.peersInfo()(any[Timeout]) returns Future.successful(List(
PeerInfo( PeerInfo(
nodeId = Alice.nodeParams.nodeId, nodeId = aliceNodeId,
state = "CONNECTED", state = "CONNECTED",
address = Some(Alice.nodeParams.publicAddresses.head.socketAddress), address = Some(NodeAddress.fromParts("localhost", 9731).get.socketAddress),
channels = 1), channels = 1),
PeerInfo( PeerInfo(
nodeId = Bob.nodeParams.nodeId, nodeId = bobNodeId,
state = "DISCONNECTED", state = "DISCONNECTED",
address = None, address = None,
channels = 1))) channels = 1)))
@ -148,8 +145,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val eclair = mock[Eclair] val eclair = mock[Eclair]
val mockService = new MockService(eclair) val mockService = new MockService(eclair)
eclair.usableBalances()(any[Timeout]) returns Future.successful(List( eclair.usableBalances()(any[Timeout]) returns Future.successful(List(
UsableBalances(canSend = 100000000 msat, canReceive = 20000000 msat, shortChannelId = ShortChannelId(1), remoteNodeId = TestConstants.Alice.keyManager.nodeKey.publicKey, isPublic = true), UsableBalances(canSend = 100000000 msat, canReceive = 20000000 msat, shortChannelId = ShortChannelId(1), remoteNodeId = aliceNodeId, isPublic = true),
UsableBalances(canSend = 400000000 msat, canReceive = 30000000 msat, shortChannelId = ShortChannelId(2), remoteNodeId = TestConstants.Alice.keyManager.nodeKey.publicKey, isPublic = false) UsableBalances(canSend = 400000000 msat, canReceive = 30000000 msat, shortChannelId = ShortChannelId(2), remoteNodeId = aliceNodeId, isPublic = false)
)) ))
Post("/usablebalances") ~> Post("/usablebalances") ~>
@ -169,9 +166,9 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val eclair = mock[Eclair] val eclair = mock[Eclair]
val mockService = new MockService(eclair) val mockService = new MockService(eclair)
eclair.getInfoResponse()(any[Timeout]) returns Future.successful(GetInfoResponse( eclair.getInfoResponse()(any[Timeout]) returns Future.successful(GetInfoResponse(
nodeId = Alice.nodeParams.nodeId, nodeId = aliceNodeId,
alias = Alice.nodeParams.alias, alias = "alice",
chainHash = Alice.nodeParams.chainHash, chainHash = ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"),
blockHeight = 9999, blockHeight = 9999,
publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil
)) ))
@ -183,7 +180,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
assert(handled) assert(handled)
assert(status == OK) assert(status == OK)
val resp = entityAs[String] val resp = entityAs[String]
assert(resp.toString.contains(Alice.nodeParams.nodeId.toString)) assert(resp.toString.contains(aliceNodeId.toString))
eclair.getInfoResponse()(any[Timeout]).wasCalled(once) eclair.getInfoResponse()(any[Timeout]).wasCalled(once)
matchTestJson("getinfo", resp) matchTestJson("getinfo", resp)
} }
@ -195,7 +192,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val channelId = "56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e" val channelId = "56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e"
val eclair = mock[Eclair] val eclair = mock[Eclair]
eclair.close(any, any)(any[Timeout]) returns Future.successful(Alice.nodeParams.nodeId.toString()) eclair.close(any, any)(any[Timeout]) returns Future.successful(aliceNodeId.toString())
val mockService = new MockService(eclair) val mockService = new MockService(eclair)
Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~> Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~>
@ -206,7 +203,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
assert(handled) assert(handled)
assert(status == OK) assert(status == OK)
val resp = entityAs[String] val resp = entityAs[String]
assert(resp.contains(Alice.nodeParams.nodeId.toString)) assert(resp.contains(aliceNodeId.toString))
eclair.close(Right(ShortChannelId(shortChannelIdSerialized)), None)(any[Timeout]).wasCalled(once) eclair.close(Right(ShortChannelId(shortChannelIdSerialized)), None)(any[Timeout]).wasCalled(once)
matchTestJson("close", resp) matchTestJson("close", resp)
} }
@ -219,7 +216,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
assert(handled) assert(handled)
assert(status == OK) assert(status == OK)
val resp = entityAs[String] val resp = entityAs[String]
assert(resp.contains(Alice.nodeParams.nodeId.toString)) assert(resp.contains(aliceNodeId.toString))
eclair.close(Left(ByteVector32.fromValidHex(channelId)), None)(any[Timeout]).wasCalled(once) eclair.close(Left(ByteVector32.fromValidHex(channelId)), None)(any[Timeout]).wasCalled(once)
matchTestJson("close", resp) matchTestJson("close", resp)
} }
@ -297,7 +294,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
check { check {
assert(handled) assert(handled)
assert(status == NotFound) assert(status == NotFound)
val resp = entityAs[ErrorResponse](JsonSupport.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(ByteVector32.Zeroes)(any[Timeout]).wasCalled(once)
} }
@ -353,31 +350,31 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty) val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty)
val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[]}""" val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[]}"""
Serialization.write(pf)(mockService.formatsWithTypeHint) === expectedSerializedPf serialization.write(pf)(mockService.formatsWithTypeHint) === expectedSerializedPf
system.eventStream.publish(pf) system.eventStream.publish(pf)
wsClient.expectMessage(expectedSerializedPf) wsClient.expectMessage(expectedSerializedPf)
val ps = PaymentSent(fixedUUID, amount = 21 msat, feesPaid = 1 msat, paymentHash = ByteVector32.Zeroes, paymentPreimage = ByteVector32.One, toChannelId = ByteVector32.Zeroes, timestamp = 1553784337711L) val ps = PaymentSent(fixedUUID, amount = 21 msat, feesPaid = 1 msat, paymentHash = ByteVector32.Zeroes, paymentPreimage = ByteVector32.One, toChannelId = ByteVector32.Zeroes, timestamp = 1553784337711L)
val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}""" val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}"""
Serialization.write(ps)(mockService.formatsWithTypeHint) === expectedSerializedPs serialization.write(ps)(mockService.formatsWithTypeHint) === 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 serialization.write(prel)(mockService.formatsWithTypeHint) === expectedSerializedPrel
system.eventStream.publish(prel) system.eventStream.publish(prel)
wsClient.expectMessage(expectedSerializedPrel) wsClient.expectMessage(expectedSerializedPrel)
val precv = PaymentReceived(amount = 21 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.One, timestamp = 1553784963659L) val precv = PaymentReceived(amount = 21 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.One, timestamp = 1553784963659L)
val expectedSerializedPrecv = """{"type":"payment-received","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" val expectedSerializedPrecv = """{"type":"payment-received","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}"""
Serialization.write(precv)(mockService.formatsWithTypeHint) === expectedSerializedPrecv serialization.write(precv)(mockService.formatsWithTypeHint) === 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 serialization.write(pset)(mockService.formatsWithTypeHint) === expectedSerializedPset
system.eventStream.publish(pset) system.eventStream.publish(pset)
wsClient.expectMessage(expectedSerializedPset) wsClient.expectMessage(expectedSerializedPset)
} }

View file

@ -21,9 +21,8 @@ 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.payment.{PaymentRequest, PaymentSettlingOnChain}
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints import fr.acinq.eclair.api.JsonSupport.CustomTypeHints
import fr.acinq.eclair.payment.PaymentRequest 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.json4s.jackson.Serialization
@ -84,10 +83,7 @@ class JsonSerializersSpec extends FunSuite with Matchers {
test("transaction serializer") { test("transaction serializer") {
implicit val formats = JsonSupport.formats 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) == "{\"txid\":\"3ef63b5d297c9dcf93f33b45b9f102733c36e8ef61da1ccf2bc132a10584be18\",\"tx\":\"0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800\"}")
} }
} }