diff --git a/README.md b/README.md index 6042f89..74a4ae2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # blockchain-scala Simple Blockchain Implementation in Scala. + +Inspired by [Daniel van Flymen](http://www.dvf.nyc/)'s blog [Learn Blockchains by Building One](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46), I started to implement this Scala version of a simple Blockchain. + +REST API service is built on the open source projects [akka](https://github.com/akka/akka) and [akka-http](https://github.com/akka/akka-http). + diff --git a/scalastyle-config.xml b/scalastyle-config.xml index 7a820fd..b316416 100644 --- a/scalastyle-config.xml +++ b/scalastyle-config.xml @@ -90,7 +90,7 @@ - + diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..10255fe --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,4 @@ +http { + host = "0.0.0.0" + port = 9090 +} \ No newline at end of file diff --git a/src/main/scala/com/fluency03/blockchain/api/Server.scala b/src/main/scala/com/fluency03/blockchain/api/Server.scala index dd060cc..a8520e2 100644 --- a/src/main/scala/com/fluency03/blockchain/api/Server.scala +++ b/src/main/scala/com/fluency03/blockchain/api/Server.scala @@ -7,34 +7,38 @@ import akka.http.scaladsl.Http.ServerBinding import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.stream.ActorMaterializer -import com.fluency03.blockchain.api.actors.{BlockRegistryActor, BlockchainRegistryActor, TransactionRegistryActor} -import com.fluency03.blockchain.api.routes.{BlockRoutes, BlockchainRoutes} +import com.fluency03.blockchain.api.actors.{BlockActor, BlockchainActor, TransactionActor} +import com.fluency03.blockchain.api.routes.{BlockRoutes, BlockchainRoutes, TransactionRoutes} +import com.typesafe.config.ConfigFactory import scala.concurrent.{Await, ExecutionContextExecutor, Future} import scala.concurrent.duration.Duration -object Server extends App with BlockchainRoutes with BlockRoutes { +object Server extends App with BlockchainRoutes with BlockRoutes with TransactionRoutes { implicit val system: ActorSystem = ActorSystem("blockchain-http-service") implicit val materializer: ActorMaterializer = ActorMaterializer() implicit val executionContext: ExecutionContextExecutor = system.dispatcher override lazy val log = Logging(system, classOf[App]) - val (interface, port) = (args(0), args(1).toInt) + val config = ConfigFactory.load() + val httpConfig = config.getConfig("http") + val (host, port) = (httpConfig.getString("host"), httpConfig.getInt("port")) - val blockchainRegistryActor: ActorRef = system.actorOf(BlockchainRegistryActor.props, "blockchainRegistryActor") - val blockRegistryActor: ActorRef = system.actorOf(BlockRegistryActor.props, "blockRegistryActor") + val blockchainActor: ActorRef = system.actorOf(BlockchainActor.props, BLOCKCHAIN_ACTOR_NAME) + val blockActor: ActorRef = system.actorOf(BlockActor.props, BLOCK_ACTOR_NAME) + val txActor: ActorRef = system.actorOf(TransactionActor.props, TX_ACTOR_NAME) - lazy val routes: Route = blockchainRoutes ~ blockRoutes + lazy val routes: Route = blockchainRoutes ~ blockRoutes ~ txRoutes val bindingFuture: Future[ServerBinding] = - Http().bindAndHandle(routes, interface, port) + Http().bindAndHandle(routes, host, port) bindingFuture.failed.foreach { ex => - log.error(ex, "Failed to bind to {}:{}!", interface, port) + log.error(ex, "Failed to bind to {}:{}!", host, port) } - log.info("Server online at http://{}:{}/", interface, port) + log.info("Server online at http://{}:{}/", host, port) Await.result(system.whenTerminated, Duration.Inf) } diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala new file mode 100644 index 0000000..b6e0ac1 --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala @@ -0,0 +1,49 @@ +package com.fluency03.blockchain.api.actors + +import akka.actor.{Actor, ActorLogging, ActorSelection, Props} +import com.fluency03.blockchain.api.actors.BlockActor._ +import com.fluency03.blockchain.api.utils.GenericMessage._ +import com.fluency03.blockchain.api.{BLOCKCHAIN_ACTOR_NAME, PARENT_UP, TX_ACTOR_NAME} +import com.fluency03.blockchain.core.Block +import scala.collection.mutable + +object BlockActor { + final case object GetBlocks + final case class CreateBlock(block: Block) + final case class GetBlock(hash: String) + final case class DeleteBlock(hash: String) + + def props: Props = Props[BlockActor] +} + +class BlockActor extends Actor with ActorLogging { + override def preStart(): Unit = log.info("{} started!", this.getClass.getSimpleName) + override def postStop(): Unit = log.info("{} stopped!", this.getClass.getSimpleName) + + val blockchainActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCKCHAIN_ACTOR_NAME) + val txActor: ActorSelection = context.actorSelection(PARENT_UP + TX_ACTOR_NAME) + + var blocks = mutable.Map.empty[String, Block] + + def receive: Receive = { + case GetBlocks => onGetBlocks() + case CreateBlock(block) => onCreateBlock(block) + case GetBlock(hash) => onGetBlock(hash) + case DeleteBlock(hash) => onDeleteBlock(hash) + } + + private[this] def onGetBlocks(): Unit = sender() ! blocks.values.toList + + private[this] def onCreateBlock(block: Block): Unit = { + blocks += (block.hash -> block) + sender() ! Response(s"Block ${block.hash} created.") + } + + private[this] def onGetBlock(hash: String): Unit = sender() ! blocks.get(hash) + + private[this] def onDeleteBlock(hash: String): Unit = { + blocks -= hash + sender() ! Response(s"Block $hash deleted.") + } + +} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/BlockRegistryActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/BlockRegistryActor.scala deleted file mode 100644 index a78b25c..0000000 --- a/src/main/scala/com/fluency03/blockchain/api/actors/BlockRegistryActor.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.fluency03.blockchain.api.actors - -import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import com.fluency03.blockchain.core.Block - -object BlockRegistryActor { - final case class ActionPerformed(description: String) - final case object GetBlocks - final case class CreateBlock(block: Block) - final case class GetBlock(hash: String) - final case class DeleteBlock(hash: String) - - def props: Props = Props[TransactionRegistryActor] -} - -class BlockRegistryActor extends Actor with ActorLogging { - import BlockRegistryActor._ - - val blockchainActor: ActorRef = context.actorOf(BlockchainRegistryActor.props) - - var blocks = Set.empty[Block] - - def receive: Receive = { - case GetBlocks => sender() ! blocks.toList - case CreateBlock(block) => - blocks += block - sender() ! ActionPerformed(s"Block ${block.hash} created.") - case GetBlock(hash) => sender() ! blocks.find(_.hash == hash) - case DeleteBlock(hash) => - blocks.find(_.hash == hash) foreach { block => blocks -= block } - sender() ! ActionPerformed(s"Block $hash deleted.") - } -} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala new file mode 100644 index 0000000..0ba8cd7 --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala @@ -0,0 +1,78 @@ +package com.fluency03.blockchain.api.actors + +import akka.actor.{Actor, ActorLogging, ActorSelection, Props} +import com.fluency03.blockchain.api.actors.BlockchainActor._ +import com.fluency03.blockchain.api.actors.TransactionActor._ +import com.fluency03.blockchain.api.utils.GenericMessage._ +import com.fluency03.blockchain.api.{BLOCK_ACTOR_NAME, PARENT_UP, TX_ACTOR_NAME} +import com.fluency03.blockchain.core.{Blockchain, Transaction} + +object BlockchainActor { + final case object GetBlockchain + final case object CreateBlockchain + final case object DeleteBlockchain + + def props: Props = Props[BlockchainActor] +} + +class BlockchainActor extends Actor with ActorLogging { + override def preStart(): Unit = log.info("{} started!", this.getClass.getSimpleName) + override def postStop(): Unit = log.info("{} stopped!", this.getClass.getSimpleName) + + val txActor: ActorSelection = context.actorSelection(PARENT_UP + TX_ACTOR_NAME) + val blockActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCK_ACTOR_NAME) + + var blockchainOpt: Option[Blockchain] = None + + def receive: Receive = { + /* ---------- Blockchain actions ---------- */ + case GetBlockchain => onGetBlockchain() + case CreateBlockchain => onCreateBlockchain() + case DeleteBlockchain => onDeleteBlockchain() + /* ---------- Transaction actions ---------- */ + case GetTransactions => onGetTransactions() + case CreateTransaction(tx) => onCreateTransaction(tx) + case GetTransaction(hash) => onGetTransaction(hash) + case DeleteTransaction(hash) => onDeleteTransaction(hash) + } + + /* ---------- Blockchain actions ---------- */ + private[this] def onGetBlockchain(): Unit = sender() ! blockchainOpt + + private[this] def onCreateBlockchain(): Unit = + if (blockchainOpt.isDefined) sender() ! Response(s"Blockchain already exists.") + else { + blockchainOpt = Some(Blockchain()) + sender() ! Response(s"Blockchain created, with difficulty ${blockchainOpt.get.difficulty}.") + } + + private[this] def onDeleteBlockchain(): Unit = + if (blockchainOpt.isDefined) { + blockchainOpt = None + sender() ! Response(s"Blockchain deleted.") + } else sender() ! Response(s"Blockchain does not exist.") + + /* ---------- Transaction actions ---------- */ + private[this] def onGetTransactions(): Unit = sender() ! { + if (blockchainOpt.isDefined) blockchainOpt.get.currentTransactions.toList + else List() + } + + private[this] def onCreateTransaction(tx: Transaction): Unit = + if (blockchainOpt.isDefined) { + blockchainOpt.get.currentTransactions += (tx.hash -> tx) + sender() ! Response(s"Transaction ${tx.hash} created.") + } else sender() ! Response(s"Blockchain does not exist.") + + private[this] def onGetTransaction(hash: String): Unit = sender() ! { + if (blockchainOpt.isDefined) blockchainOpt.get.currentTransactions(hash) + else None + } + + private[this] def onDeleteTransaction(hash: String): Unit = + if (blockchainOpt.isDefined) { + blockchainOpt.get.currentTransactions -= hash + sender() ! Response(s"Transaction $hash deleted.") + } else sender() ! Response(s"Blockchain does not exist.") + +} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainRegistryActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainRegistryActor.scala deleted file mode 100644 index ef114c1..0000000 --- a/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainRegistryActor.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.fluency03.blockchain.api.actors - -import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import com.fluency03.blockchain.core.Blockchain - -object BlockchainRegistryActor { - final case class ActionPerformed(description: String) - final case object GetBlockchain - final case object CreateBlockchain - final case object DeleteBlockchain - - def props: Props = Props[BlockchainRegistryActor] -} - -class BlockchainRegistryActor extends Actor with ActorLogging { - import BlockchainRegistryActor._ - - val blockActor: ActorRef = context.actorOf(BlockRegistryActor.props) - - var blockchainOpt: Option[Blockchain] = None - - def receive: Receive = { - case GetBlockchain => sender() ! blockchainOpt.get - case CreateBlockchain => - blockchainOpt = Some(Blockchain()) - sender() ! ActionPerformed(s"Blockchain created, with difficulty ${blockchainOpt.get.difficulty}.") - case DeleteBlockchain => - blockchainOpt = None - sender() ! ActionPerformed(s"Blockchain deleted.") - } -} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala new file mode 100644 index 0000000..193c8c8 --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala @@ -0,0 +1,38 @@ +package com.fluency03.blockchain.api.actors + +import akka.actor.{Actor, ActorLogging, ActorSelection, Props} +import com.fluency03.blockchain.api.actors.TransactionActor._ +import com.fluency03.blockchain.api.utils.GenericMessage._ +import com.fluency03.blockchain.api.{BLOCKCHAIN_ACTOR_NAME, BLOCK_ACTOR_NAME, PARENT_UP} +import com.fluency03.blockchain.core.Transaction + +object TransactionActor { + final case object GetTransactions + final case class CreateTransaction(tx: Transaction) + final case class GetTransaction(hash: String) + final case class DeleteTransaction(hash: String) + + def props: Props = Props[TransactionActor] +} + +class TransactionActor extends Actor with ActorLogging { + override def preStart(): Unit = log.info("{} started!", this.getClass.getSimpleName) + override def postStop(): Unit = log.info("{} stopped!", this.getClass.getSimpleName) + + val blockchainActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCKCHAIN_ACTOR_NAME) + val blockActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCK_ACTOR_NAME) + + def receive: Receive = { + case _ => blockchainActor forward _ + } + +// +// { +// case msg @ GetTransactions => blockchainActor forward msg +// case msg: CreateTransaction => blockchainActor forward msg +// case msg: GetTransaction => blockchainActor forward msg +// case msg: DeleteTransaction => blockchainActor forward msg +// } + + +} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/TransactionRegistryActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/TransactionRegistryActor.scala deleted file mode 100644 index b3643c3..0000000 --- a/src/main/scala/com/fluency03/blockchain/api/actors/TransactionRegistryActor.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.fluency03.blockchain.api.actors - -import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import com.fluency03.blockchain.core.{Block, Transaction} - -object TransactionRegistryActor { - final case class ActionPerformed(description: String) - final case object GetTransactions - final case class CreateTransaction(tx: Transaction) - final case class GetTransaction(hash: String) - final case class DeleteTransaction(hash: String) - - def props: Props = Props[TransactionRegistryActor] -} - -class TransactionRegistryActor extends Actor with ActorLogging { - import TransactionRegistryActor._ - - var transactions = Set.empty[Transaction] - - def receive: Receive = { - case GetTransactions => sender() ! transactions.toList - case CreateTransaction(tx) => - transactions += tx - sender() ! ActionPerformed(s"Transaction ${tx.hash} created.") - case GetTransaction(hash) => sender() ! transactions.find(_.hash == hash) - case DeleteTransaction(hash) => - transactions.find(_.hash == hash) foreach { tx => transactions -= tx } - sender() ! ActionPerformed(s"Transaction $hash deleted.") - } -} diff --git a/src/main/scala/com/fluency03/blockchain/api/package.scala b/src/main/scala/com/fluency03/blockchain/api/package.scala index f924bd9..46eeb35 100644 --- a/src/main/scala/com/fluency03/blockchain/api/package.scala +++ b/src/main/scala/com/fluency03/blockchain/api/package.scala @@ -1,9 +1,16 @@ package com.fluency03.blockchain -import com.fluency03.blockchain.core.Block +import com.fluency03.blockchain.core.{Block, Transaction} package object api { type Blocks = List[Block] + type Transactions = List[Transaction] + + lazy val BLOCK_ACTOR_NAME = "blockActor" + lazy val BLOCKCHAIN_ACTOR_NAME = "blockchainActor" + lazy val TX_ACTOR_NAME = "txActor" + + lazy val PARENT_UP = "../" } diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala index 6c2c42c..7f7685a 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala @@ -9,7 +9,9 @@ import akka.http.scaladsl.server.directives.MethodDirectives.{delete, get, post} import akka.http.scaladsl.server.directives.PathDirectives.path import akka.http.scaladsl.server.directives.RouteDirectives.complete import akka.pattern.ask -import com.fluency03.blockchain.api.actors.BlockRegistryActor._ +import com.fluency03.blockchain.api.Blocks +import com.fluency03.blockchain.api.actors.BlockActor._ +import com.fluency03.blockchain.api.utils.GenericMessage.Response import com.fluency03.blockchain.core.Block import scala.concurrent.Future @@ -17,35 +19,35 @@ import scala.concurrent.Future trait BlockRoutes extends Routes { lazy val log = Logging(system, classOf[BlockRoutes]) - def blockRegistryActor: ActorRef + def blockActor: ActorRef lazy val blockRoutes: Route = pathPrefix("blocks") { pathEnd { get { - val blocks: Future[List[Block]] = (blockRegistryActor ? GetBlocks).mapTo[List[Block]] + val blocks: Future[Blocks] = (blockActor ? GetBlocks).mapTo[Blocks] complete(blocks) } ~ post { entity(as[Block]) { block => - val blockCreated: Future[ActionPerformed] = (blockRegistryActor ? CreateBlock(block)).mapTo[ActionPerformed] - onSuccess(blockCreated) { performed => - log.info("Created user [{}]: {}", block.hash, performed.description) - complete((StatusCodes.Created, performed)) + val blockCreated: Future[Response] = (blockActor ? CreateBlock(block)).mapTo[Response] + onSuccess(blockCreated) { resp => + log.info("Created block [{}]: {}", block.hash, resp.message) + complete((StatusCodes.Created, resp)) } } } } ~ path(Segment) { hash => get { - val maybeBlock: Future[Option[Block]] = (blockRegistryActor ? GetBlock(hash)).mapTo[Option[Block]] + val maybeBlock: Future[Option[Block]] = (blockActor ? GetBlock(hash)).mapTo[Option[Block]] rejectEmptyResponse { complete(maybeBlock) } } ~ delete { - val blockDeleted: Future[ActionPerformed] = (blockRegistryActor ? DeleteBlock(hash)).mapTo[ActionPerformed] - onSuccess(blockDeleted) { performed => - log.info("Deleted block [{}]: {}", hash, performed.description) - complete((StatusCodes.OK, performed)) + val blockDeleted: Future[Response] = (blockActor ? DeleteBlock(hash)).mapTo[Response] + onSuccess(blockDeleted) { resp => + log.info("Deleted block [{}]: {}", hash, resp.message) + complete((StatusCodes.OK, resp)) } } } diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/BlockchainRoutes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/BlockchainRoutes.scala index cc120da..5927aaa 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/BlockchainRoutes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/BlockchainRoutes.scala @@ -8,7 +8,8 @@ import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.directives.MethodDirectives.{delete, get, post} import akka.http.scaladsl.server.directives.RouteDirectives.complete import akka.pattern.ask -import com.fluency03.blockchain.api.actors.BlockchainRegistryActor._ +import com.fluency03.blockchain.api.actors.BlockchainActor._ +import com.fluency03.blockchain.api.utils.GenericMessage.Response import com.fluency03.blockchain.core.Blockchain import org.json4s.JsonAST.JValue @@ -17,31 +18,29 @@ import scala.concurrent.Future trait BlockchainRoutes extends Routes { lazy val log = Logging(system, classOf[BlockchainRoutes]) - def blockchainRegistryActor: ActorRef + def blockchainActor: ActorRef lazy val blockchainRoutes: Route = pathPrefix("blockchain") { pathEnd { get { - val users: Future[Blockchain] = (blockchainRegistryActor ? GetBlockchain).mapTo[Blockchain] - complete(users) + val blockchain: Future[Option[Blockchain]] = (blockchainActor ? GetBlockchain).mapTo[Option[Blockchain]] + rejectEmptyResponse { complete(blockchain) } } ~ post { entity(as[JValue]) { _ => - val blockchainCreated: Future[ActionPerformed] = - (blockchainRegistryActor ? CreateBlockchain).mapTo[ActionPerformed] - onSuccess(blockchainCreated) { performed => - log.info("Created Blockchain: {}", performed.description) - complete((StatusCodes.Created, performed)) + val blockchainCreated: Future[Response] = (blockchainActor ? CreateBlockchain).mapTo[Response] + onSuccess(blockchainCreated) { resp => + log.info("Created Blockchain: {}", resp.message) + complete((StatusCodes.Created, resp)) } } } ~ delete { - val blockchainDeleted: Future[ActionPerformed] = - (blockchainRegistryActor ? DeleteBlockchain).mapTo[ActionPerformed] - onSuccess(blockchainDeleted) { performed => - log.info("Deleted Blockchain: {}", performed.description) - complete((StatusCodes.OK, performed)) + val blockchainDeleted: Future[Response] = (blockchainActor ? DeleteBlockchain).mapTo[Response] + onSuccess(blockchainDeleted) { resp => + log.info("Deleted Blockchain: {}", resp.message) + complete((StatusCodes.OK, resp)) } } } diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/Routes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/Routes.scala index ec29adc..ee4916c 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/Routes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/Routes.scala @@ -1,9 +1,8 @@ package com.fluency03.blockchain.api.routes import akka.actor.ActorSystem -import akka.event.Logging import akka.util.Timeout -import com.fluency03.blockchain.api.JsonSupport +import com.fluency03.blockchain.api.utils.JsonSupport import scala.concurrent.duration._ @@ -11,7 +10,7 @@ trait Routes extends JsonSupport { // we leave these abstract, since they will be provided by the App implicit def system: ActorSystem - // Required by the `ask` (?) method below + // Required by the `ask` (?) method implicit lazy val timeout: Timeout = Timeout(5.seconds) // usually we'd obtain the timeout from the system's configuration } diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala index 727f180..92c5c9d 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala @@ -9,45 +9,47 @@ import akka.http.scaladsl.server.directives.MethodDirectives.{delete, get, post} import akka.http.scaladsl.server.directives.PathDirectives.path import akka.http.scaladsl.server.directives.RouteDirectives.complete import akka.pattern.ask -import com.fluency03.blockchain.api.actors.BlockRegistryActor._ -import com.fluency03.blockchain.core.Block +import com.fluency03.blockchain.api.Transactions +import com.fluency03.blockchain.api.actors.TransactionActor._ +import com.fluency03.blockchain.api.utils.GenericMessage.Response +import com.fluency03.blockchain.core.Transaction import scala.concurrent.Future trait TransactionRoutes extends Routes { lazy val log = Logging(system, classOf[TransactionRoutes]) - def txRoutesRegistryActor: ActorRef + def txActor: ActorRef -// lazy val transactionRoutes: Route = -// pathPrefix("transactions") { -// pathEnd { -// get { -// val blocks: Future[List[Block]] = (txRoutesRegistryActor ? GetBlocks).mapTo[List[Block]] -// complete(blocks) -// } ~ -// post { -// entity(as[Block]) { block => -// val blockCreated: Future[ActionPerformed] = (txRoutesRegistryActor ? CreateBlock(block)).mapTo[ActionPerformed] -// onSuccess(blockCreated) { performed => -// log.info("Created user [{}]: {}", block.hash, performed.description) -// complete((StatusCodes.Created, performed)) -// } -// } -// } -// } ~ -// path(Segment) { hash => -// get { -// val maybeBlock: Future[Option[Block]] = (txRoutesRegistryActor ? GetBlock(hash)).mapTo[Option[Block]] -// rejectEmptyResponse { complete(maybeBlock) } -// } ~ -// delete { -// val blockDeleted: Future[ActionPerformed] = (txRoutesRegistryActor ? DeleteBlock(hash)).mapTo[ActionPerformed] -// onSuccess(blockDeleted) { performed => -// log.info("Deleted block [{}]: {}", hash, performed.description) -// complete((StatusCodes.OK, performed)) -// } -// } -// } -// } + lazy val txRoutes: Route = + pathPrefix("transactions") { + pathEnd { + get { + val transactions: Future[Transactions] = (txActor ? GetTransactions).mapTo[Transactions] + complete(transactions) + } ~ + post { + entity(as[Transaction]) { tx => + val txCreated: Future[Response] = (txActor ? CreateTransaction(tx)).mapTo[Response] + onSuccess(txCreated) { resp => + log.info("Created transaction [{}]: {}", tx.hash, resp.message) + complete((StatusCodes.Created, resp)) + } + } + } + } ~ + path(Segment) { hash => + get { + val maybeTx: Future[Option[Transaction]] = (txActor ? GetTransaction(hash)).mapTo[Option[Transaction]] + rejectEmptyResponse { complete(maybeTx) } + } ~ + delete { + val txDeleted: Future[Response] = (txActor ? DeleteTransaction(hash)).mapTo[Response] + onSuccess(txDeleted) { resp => + log.info("Deleted transaction [{}]: {}", hash, resp.message) + complete((StatusCodes.OK, resp)) + } + } + } + } } diff --git a/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala b/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala new file mode 100644 index 0000000..e0cfcf8 --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala @@ -0,0 +1,7 @@ +package com.fluency03.blockchain.api.utils + +object GenericMessage { + + final case class Response(message: String) + +} diff --git a/src/main/scala/com/fluency03/blockchain/api/JsonSupport.scala b/src/main/scala/com/fluency03/blockchain/api/utils/JsonSupport.scala similarity index 89% rename from src/main/scala/com/fluency03/blockchain/api/JsonSupport.scala rename to src/main/scala/com/fluency03/blockchain/api/utils/JsonSupport.scala index 0886e28..55601d2 100644 --- a/src/main/scala/com/fluency03/blockchain/api/JsonSupport.scala +++ b/src/main/scala/com/fluency03/blockchain/api/utils/JsonSupport.scala @@ -1,4 +1,4 @@ -package com.fluency03.blockchain.api +package com.fluency03.blockchain.api.utils import de.heikoseeberger.akkahttpjson4s.Json4sSupport import org.json4s.native.Serialization diff --git a/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala b/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala index b19ae9d..43deb2b 100644 --- a/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala +++ b/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala @@ -1,8 +1,9 @@ package com.fluency03.blockchain.core import com.fluency03.blockchain.Util.getCurrentTimestamp +import com.fluency03.blockchain.core.Blockchain._ -import scala.collection.mutable.ListBuffer +import scala.collection.mutable /** * Blockchain with difficulty and the chain of Blocks. @@ -10,18 +11,18 @@ import scala.collection.mutable.ListBuffer * @param chain Chain of Blocks */ case class Blockchain(difficulty: Int = 4, chain: List[Block] = List(Block.genesisBlock)) { - val currentTransactions: ListBuffer[Transaction] = new ListBuffer[Transaction]() + val currentTransactions: mutable.Map[String, Transaction] = mutable.Map.empty[String, Transaction] def addBlock(newBlockData: String): Blockchain = { - Blockchain(difficulty, mineNextBlock(newBlockData).addTransactions(currentTransactions.toList) :: chain) + Blockchain(difficulty, mineNextBlock(newBlockData).addTransactions(currentTransactions.values.toList) :: chain) } def addBlock(newBlock: Block): Blockchain = { Blockchain(difficulty, newBlock :: chain) } - def addTransaction(t: Transaction): Blockchain = { - currentTransactions += t + def addTransaction(tx: Transaction): Blockchain = { + currentTransactions += (tx.hash -> tx) this } @@ -32,7 +33,7 @@ case class Blockchain(difficulty: Int = 4, chain: List[Block] = List(Block.genes addTransaction(Transaction(sender, receiver, amount, timestamp)) def addTransactions(trans: List[Transaction]): Blockchain = { - currentTransactions ++= trans + currentTransactions ++= trans.map(tx => (tx.hash, tx)) this } @@ -46,16 +47,27 @@ case class Blockchain(difficulty: Int = 4, chain: List[Block] = List(Block.genes lastHeader.index + 1, lastHeader.hash, newBlockData, - currentTransactions.toList, + currentTransactions.values.toList, getCurrentTimestamp, difficulty) } + def isValid: Boolean = chain match { + case Nil => throw new NoSuchElementException("Blockchain is Empty!") + case _ => isValidChain(chain, difficulty) + } + } object Blockchain { def apply(difficulty: Int): Blockchain = new Blockchain(difficulty, List(Block.genesis(difficulty))) + def isValidChain(chain: List[Block], difficulty: Int): Boolean = chain match { + case Nil => true + case g :: Nil => g.previousHash == ZERO64 && g.isValid(difficulty) + case a :: b :: tail => a.previousHash == b.hash && a.isValid(difficulty) && isValidChain(b :: tail, difficulty) + } + } diff --git a/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala b/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala index cc892d9..6716f69 100644 --- a/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala +++ b/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala @@ -2,6 +2,7 @@ package com.fluency03.blockchain.core import org.scalatest.{FlatSpec, Matchers} +import scala.collection.mutable import scala.collection.mutable.ListBuffer class BlockchainTest extends FlatSpec with Matchers { @@ -22,7 +23,7 @@ class BlockchainTest extends FlatSpec with Matchers { blockchain.chain shouldEqual List(genesis) blockchain.lastBlock().isEmpty shouldEqual false blockchain.lastBlock().get shouldEqual genesis - blockchain.currentTransactions shouldEqual new ListBuffer[Transaction]() + blockchain.currentTransactions shouldEqual mutable.Map.empty[String, Transaction] } "A new Blockchain with different difficulty" should "have all default values but the difficulty." in { @@ -30,25 +31,28 @@ class BlockchainTest extends FlatSpec with Matchers { blockchainOf5.chain shouldEqual List(genesisOf5) blockchainOf5.lastBlock().isEmpty shouldEqual false blockchainOf5.lastBlock().get shouldEqual genesisOf5 - blockchainOf5.currentTransactions shouldEqual new ListBuffer[Transaction]() + blockchainOf5.currentTransactions shouldEqual mutable.Map.empty[String, Transaction] } "Add a Transaction to a Blockchain" should "add these Transactions to its currentTransactions collection." in { - val trans = new ListBuffer[Transaction]() + val trans = mutable.Map.empty[String, Transaction] blockchain.addTransaction(t1) - trans += t1 + trans += (t1.hash -> t1) blockchain.currentTransactions shouldEqual trans blockchain.addTransaction(t2) blockchain.addTransaction(t3) - trans ++= t2 :: t3 :: Nil + trans += (t2.hash -> t2) + trans += (t3.hash -> t3) blockchain.currentTransactions shouldEqual trans } "Add a List of Transaction to a Blockchain" should "add these Transactions to its currentTransactions collection." in { - val trans = new ListBuffer[Transaction]() + val trans = mutable.Map.empty[String, Transaction] blockchainOf5.addTransactions(t2 :: t3 :: t4 :: Nil) - trans ++= t2 :: t3 :: t4 :: Nil + trans += (t2.hash -> t2) + trans += (t3.hash -> t3) + trans += (t4.hash -> t4) blockchainOf5.currentTransactions shouldEqual trans }