bitcoin-atm

bitcoin atm for pyc inc.

git clone https://9o.is/git/bitcoin-atm.git

commit 13f4fa67f24a1238ffee71a0227a2bca2d65aa17
parent a747e02eb5d6efe7f8ab3a8937c9ec56c5213797
Author: Jul <jul@9o.is>
Date:   Sat,  9 Aug 2014 10:00:06 -0700

reorganized package structure. Moved bitcoin lib to akka extension. created unfinished bill acceptor interface. cleaned up interface for comet

Diffstat:
Msrc/main/resources/application.conf | 34++++++++++++++++++++++------------
Asrc/main/resources/reference.conf | 5+++++
Msrc/main/scala/bootstrap/liftweb/Boot.scala | 35++++++++++++++++++-----------------
Asrc/main/scala/inc/pyc/bitcoin/BitcoinService.scala | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/BitcoinSystem.scala | 31+++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/PriceTicker.scala | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/Wallet.scala | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BitStamp.scala | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BitcoinExchange.scala | 20++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BitcoinPriceTicker.scala | 33+++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BitcoinServiceActor.scala | 15+++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BitcoinWallet.scala | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BlockChain.scala | 35+++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/BtcWallet.scala | 26++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/HttpBitcoinService.scala | 37+++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/JsonRPC.scala | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/bitcoin/service/WsBitcoinService.scala | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/main/scala/inc/pyc/chimera/bitcoin/BitcoinServices.scala | 71-----------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/PriceTicker.scala | 144-------------------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/Wallet.scala | 158-------------------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BitStamp.scala | 49-------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinExchange.scala | 21---------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinPriceTicker.scala | 34----------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinServiceActor.scala | 16----------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinWallet.scala | 222-------------------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BlockChain.scala | 37-------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/BtcWallet.scala | 28----------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/HttpBitcoinService.scala | 37-------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/JsonRPC.scala | 137-------------------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/bitcoin/service/WsBitcoinService.scala | 201-------------------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/comet/PYCActor.scala | 33---------------------------------
Msrc/main/scala/inc/pyc/chimera/config/Machine.scala | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Asrc/main/scala/inc/pyc/chimera/lib/Network.scala | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/bill/acceptor/BillAcceptor.scala | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/APEX.scala | 13+++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/Driver.scala | 24++++++++++++++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/JCM.scala | 13+++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/package.scala | 12++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/currency/Currency.scala | 13+++++++++++++
Asrc/main/scala/inc/pyc/chimera/lib/currency/USD.scala | 9+++++++++
Asrc/main/scala/inc/pyc/chimera/lib/currency/package.scala | 12++++++++++++
Msrc/main/scala/inc/pyc/chimera/lycia/Lycia.scala | 32++------------------------------
Msrc/main/scala/inc/pyc/chimera/model/Transaction.scala | 25++++++++++++++++++++++---
Msrc/main/scala/inc/pyc/chimera/snippet/BitcoinAddressScanner.scala | 24++++++++++++++++--------
Dsrc/main/scala/inc/pyc/chimera/snippet/ClientActorBridge.scala | 107-------------------------------------------------------------------------------
Asrc/main/scala/inc/pyc/chimera/snippet/Comet.scala | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/main/scala/inc/pyc/chimera/snippet/Network.scala | 120-------------------------------------------------------------------------------
Dsrc/main/scala/inc/pyc/chimera/snippet/PriceTicker.scala | 33---------------------------------
Dsrc/main/scala/inc/pyc/chimera/snippet/Transaction.scala | 62--------------------------------------------------------------
Msrc/main/scala/inc/pyc/chimera/snippet/UtilSnips.scala | 8+-------
Asrc/main/scala/inc/pyc/lift_akka/ClientActorBridge.scala | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main/webapp/app/ActorsBridge.js | 116+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/main/webapp/app/App.js | 43+++++++++++++++++++++----------------------
Msrc/main/webapp/index.html | 8++++++--
Asrc/main/webapp/malfunction.html | 4++++
Msrc/main/webapp/templates-hidden/base-wrap.html | 2+-
56 files changed, 1928 insertions(+), 1661 deletions(-)

diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf @@ -1,18 +1,28 @@ -btcwallet { - wallet-uri = "wss://localhost:8332/ws" - rpc-user = "jcabrra" - rpc-pass = "ZBrGj5RBGnip" - wallet-pass = "GlwNO5dm7qa1" - keystore-file = "src/main/resources/btcwallet_keystore.jks" - keystore-pass = "keystorepass" +chimera { + id = 12345 + name = test + currency = USD + bill-acceptor = JCM } -blockchain { - wallet-uri = "https://rpc.blockchain.info" - rpc-user = "c83ff98f-1208-4167-bb2f-48d7a9773194" - rpc-pass = "Pe6MK1kqeZIa" - wallet-pass = "GlwNO5dm7qa1" +bitcoin { + + btcwallet { + wallet-uri = "wss://localhost:8332/ws" + rpc-user = "jcabrra" + rpc-pass = "ZBrGj5RBGnip" + wallet-pass = "GlwNO5dm7qa1" + keystore-file = "src/main/resources/btcwallet_keystore.jks" + keystore-pass = "keystorepass" + } + + blockchain { + wallet-uri = "https://rpc.blockchain.info" + rpc-user = "c83ff98f-1208-4167-bb2f-48d7a9773194" + rpc-pass = "Pe6MK1kqeZIa" + wallet-pass = "GlwNO5dm7qa1" + } } akka { diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf @@ -0,0 +1,4 @@ +bitcoin { + wallet = "BlockChain" + priceticker = "BitStamp" +} +\ No newline at end of file diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -12,11 +12,12 @@ import net.liftweb.sitemap._ import Loc._ import inc.pyc.chimera._ import model._ +import lycia._ import config._ -import bitcoin._ -import PriceTicker.commands._ -import Wallet.commands._ -import snippet.{Network, TransactionState} +import lib._ +import Machine._ +import inc.pyc.bitcoin._ + /** * A class that's instantiated early and run. It allows the application @@ -39,6 +40,7 @@ class Boot extends Loggable { LiftRules.setSiteMap(SiteMap(List( Menu.i("Bitcoin ATM") / "index", Menu.i("Disconnected") / "disconnected", + Menu.i("Malfunction") / "malfunction", Menu.i("Error") / "error" >> Hidden, Menu.i("404") / "404" >> Hidden ): _*)) @@ -63,21 +65,20 @@ class Boot extends Loggable { // Does the following at system shutdown LiftRules.unloadHooks.append { - () => - import inc.pyc.chimera._ - import bitcoin.service._ - import lycia._ - - BitcoinServices.system.shutdown() - Lycia.system.shutdown() - Network.system.shutdown() - TransactionState.system.shutdown() + () => + system.shutdown() } - // Initialize actors - PriceTicker ! Tick - Wallet ! InitBalance - Network ! "init" + // Initialize price ticker + priceTicker.changeBitcoinService(Lycia.servicePriceTicker) + priceTicker.percentage(Lycia.percentProfit) + + // Initialize bitcoin wallet + wallet.changeBitcoinService(Lycia.serviceWallet) + wallet.initBalance + + // Initialize network monitor + Network(system).ping } def setDB(): Unit = { diff --git a/src/main/scala/inc/pyc/bitcoin/BitcoinService.scala b/src/main/scala/inc/pyc/bitcoin/BitcoinService.scala @@ -0,0 +1,41 @@ +package inc.pyc.bitcoin + +import service._ +import akka.actor.Props + +/** + * The different choices for bitcoin services. + */ +object BitcoinService extends Enumeration { + type BitcoinService = Value + val BitStamp, BlockChain, BtcWallet = Value + + def priceTickers: Map[BitcoinService, Props] = Map( + BitStamp -> Props[BitStamp], + BlockChain -> Props[BlockChain] + ) + + def wallets: Map[BitcoinService, Props] = Map( + BtcWallet -> Props[BtcWallet], + BlockChain -> Props[BlockChain] + ) + + def getPriceTicker(s: String) = get(s, priceTickers) + def getPriceTicker(s: Value) = get(s, priceTickers) + def getWallet(s: String) = get(s, wallets) + def getWallet(s: Value) = get(s, wallets) + + private def get(s: Value, m: Map[BitcoinService, Props]) = { + m.filter(_._1 == s).map(_._2).head + } + + private def get(s: String, m: Map[BitcoinService, Props]) = { + m.filter(_._1.toString.toLowerCase == s.toLowerCase).map(_._2).head + } +} + + +/** + * Command to change the bitcoin service provider. + */ +case class ChangeBitcoinService(service: BitcoinService.Value) diff --git a/src/main/scala/inc/pyc/bitcoin/BitcoinSystem.scala b/src/main/scala/inc/pyc/bitcoin/BitcoinSystem.scala @@ -0,0 +1,30 @@ +package inc.pyc.bitcoin + +import akka.actor._ +import akka.event._ +import com.typesafe.config._ +import inc.pyc.lift_akka.LiftActorEventBus + + +/** + * Main bitcoin actor services + */ +object BitcoinSystem { + + /** + * Main config + */ + val config = ConfigFactory.load().getConfig("bitcoin") + + /** + * Event Bus for Bitcoin services. + */ + val bus = new LookupBitcoin +} + +/** + * PubSub for lift actors + */ +class LookupBitcoin extends LiftActorEventBus { + override protected def mapSize: Int = 50 +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/PriceTicker.scala b/src/main/scala/inc/pyc/bitcoin/PriceTicker.scala @@ -0,0 +1,158 @@ +package inc.pyc.bitcoin + +import service._ +import akka.actor._ +import akka.event._ +import akka.util.Timeout +import SupervisorStrategy._ +import akka.pattern.{ask, pipe} +import scala.concurrent._ +import duration._ +import inc.pyc.lift_akka.EventUpdate + + +object PriceTicker extends ExtensionId[PriceTickerImpl] with ExtensionIdProvider { + override def lookup = PriceTicker + override def createExtension(system: ExtendedActorSystem) = new PriceTickerImpl(system) + + /** + * Subscription topics for event bus. + */ + val newPrice = "newPrice" +} + +/** + * Price Ticker API implementation + */ +class PriceTickerImpl(system: ExtendedActorSystem) extends Extension { + import PriceTickerCommands._ + import system.dispatcher + + implicit private val timeout: Timeout = Timeout(5 seconds) + + /** + * The main actor that handles communications. + */ + private val actor = system.actorOf(Props[PriceTicker], "PriceTicker") + + /** + * The ticker that updates the price every 5 minutes. + */ + private val ticker = + system.scheduler.schedule(0 milliseconds, 5 minutes, actor, Tick) + + def price() = (actor ? GetPrice).mapTo[Price] + + def tick(): Unit = actor ! Tick + + def percentage(profit: Double): Unit = actor ! Percentage(profit) + + def gossipPrice(): Unit = actor ! GossipPrice + + def changeBitcoinService(service: BitcoinService.Value) = + actor ! ChangeBitcoinService(service) +} + + +/** + * Main Price Ticker actor that creates child actor and uses the chosen, + * configured service to get the price per bitcoin. + */ +class PriceTicker extends Actor with ActorLogging { + import PriceTickerCommands._ + import context.dispatcher + + override val supervisorStrategy = + OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { + case _: java.net.ConnectException => Resume + case t => + super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate) + } + + /** The current service to use to check price. */ + private var service: ActorRef = createServiceActor() + + context.watch(service) + + /** Price per bitcoin */ + private var price: String = "0.00" + + /** Percentage price over market */ + private var percentage: Double = 0 + + def receive = { + case Tick => + service ! Tick + + case GetPrice => + sender ! Price(priceWithPercentage) + + case GossipPrice => + self ! Price(price) + + case Percentage(profit) => + percentage = profit + self ! Price(price) + + case Price(newPrice) => + price = newPrice + BitcoinSystem.bus.publish(EventUpdate(PriceTicker.newPrice, Price(priceWithPercentage))) + + case ChangeBitcoinService(provider) => + if (context.child(provider.toString).isEmpty) { + context.unwatch(service) + context.stop(service) + service = createServiceActor(provider) + context.watch(service) + } + + case _ => + log.warning("Received Unknown Message") + } + + private def priceWithPercentage: String = { + val p = price.toDouble + val withPercentage = p + (p * (percentage * 0.01)) + Price(withPercentage toString) format + } + + private def createServiceActor(service: BitcoinService.Value): ActorRef = createServiceActor(service.toString) + + private def createServiceActor(service: String = BitcoinSystem.config.getString("priceticker")): ActorRef = { + context.actorOf(BitcoinService.getPriceTicker(service), service) + } +} + + +private[bitcoin] object PriceTickerCommands { + /** + * Price per bitcoin. + */ + case class Price(price: String) { + def format: String = "%1.2f" format (price.toDouble) + def double: Double = price.toDouble + } + + /** + * Command to send a broadcast of the buy price. + * Comet will be listening to update the UI. + */ + case object GossipPrice + + /** + * Command to send a unicast of the buy price. + */ + case object GetPrice + + /** + * Command to set a percentage price over market. + * If price over market is 5%, send 5, not 0.05, as the profit value. + */ + case class Percentage(profit: Double) + + /** + * Command to update the price per bitcoin. + * Sent frequently to update the price in the UI. + */ + case object Tick +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/Wallet.scala b/src/main/scala/inc/pyc/bitcoin/Wallet.scala @@ -0,0 +1,168 @@ +package inc.pyc.bitcoin + +import service.BitcoinJsonRPC +import akka.actor._ +import akka.event._ +import akka.util._ +import SupervisorStrategy._ +import akka.pattern.{ask, pipe} +import scala.concurrent._ +import duration._ +import BitcoinJsonRPC._ +import inc.pyc.lift_akka.EventUpdate + + +object Wallet extends ExtensionId[WalletImpl] with ExtensionIdProvider { + override def lookup = Wallet + override def createExtension(system: ExtendedActorSystem) = new WalletImpl(system) + + + /** + * Subscription topics for event bus. + */ + val newBalance = "newBalance" +} + + +/** + * Wallet API implementation + */ +class WalletImpl(system: ExtendedActorSystem) extends Extension { + import WalletCommands._ + + implicit private val timeout: Timeout = Timeout(5 seconds) + + private val actor = system.actorOf(Props[Wallet], "BitcoinWallet") + + def initBalance(): Unit = + actor ! InitBalance + + def gossipBalance(): Unit = + actor ! GossipBalance + + def createRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) = + (actor ? CreateRawTransaction(inputs, receivers)).mapTo[String] + + def listUnspentTransactions(minConfirmations: BigDecimal = 1, maxConfirmations: BigDecimal = 999999) = + (actor ? ListUnspentTransactions(minConfirmations, maxConfirmations)).mapTo[Seq[UnspentTransaction]] + + def sendRawTransaction(signedTransaction: String) = + (actor ? SendRawTransaction(signedTransaction)).mapTo[String] + + def getBalance(): Future[String] = + (actor ? GetBalance).mapTo[String] + + def getNewAddress(): Future[String] = + (actor ? GetNewAddress).mapTo[String] + + def getRawTransaction(transactionHash: String) = + (actor ? GetRawTransaction(transactionHash)).mapTo[RawTransaction] + + def signRawTransaction(transaction: String): Future[SignedTransaction] = + (actor ? SignRawTransaction(transaction)).mapTo[SignedTransaction] + + def validateAddress(address: String): Future[AddressValidation] = + (actor ? ValidateAddress(address)).mapTo[AddressValidation] + + def changeBitcoinService(service: BitcoinService.Value) = + actor ! ChangeBitcoinService(service) +} + + +/** + * Main Bitcoin Wallet actor that creates child actor and uses the chosen, + * configured service as the bitcoin wallet. + */ +class Wallet extends Actor with ActorLogging { + import WalletCommands._ + import context.dispatcher + + implicit val timeout: Timeout = Timeout(5 seconds) + + override val supervisorStrategy = + OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { + case _: java.net.ConnectException => Resume + case t => + super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate) + } + + /** The current service to use to check price. */ + private var service: ActorRef = createServiceActor() + + context.watch(service) + + /** Balance remaining in the wallet */ + private var balance: Double = 0 + + def receive = { + case InitBalance => + val newBalance = (service ? GetBalance).mapTo[String] + newBalance map { newBalance => + balance = newBalance.toDouble + } + + case GetBalance => + sender ! Balance(balance) + + case GossipBalance => + self ! Balance(balance) + + case Balance(newBalance) => + balance = newBalance + BitcoinSystem.bus.publish(EventUpdate(Wallet.newBalance, Balance(balance))) + + case ChangeBitcoinService(provider) => + if (context.child(provider.toString).isEmpty) { + context.unwatch(service) + context.stop(service) + service = createServiceActor(provider) + context.watch(service) + } + + case ValidateAddress(address) => + val extracted = extractFromBitcoinUri(address) + service ? ValidateAddress(extracted) pipeTo sender + + + case _ => + log.warning("Received Unknown Message") + } + + private def createServiceActor(service: BitcoinService.Value): ActorRef = createServiceActor(service.toString) + + private def createServiceActor(service: String = BitcoinSystem.config.getString("wallet")): ActorRef = { + context.actorOf(BitcoinService.getWallet(service), service) + } + + /** + * Extracts bitcoin address from URI. + * + * Note the regex is not what the bitcoin address should exactly be, + * but it works for extraction. + */ + private def extractFromBitcoinUri(uri: String) = { + val r = "(bitcoin:)?([a-zA-Z0-9]{1,60})(/*?.*)?".r + uri match { + case r(_, address, _) => address + case _ => "" + } + } +} + + +private[bitcoin] object WalletCommands { + /** + * Balance remaining in bitcoin wallet. + */ + case class Balance(remaining: Double) + + /** + * Command to send a broadcast of the balance. + */ + case object GossipBalance + + /** + * Initializes the balance in wallet + */ + case object InitBalance +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BitStamp.scala b/src/main/scala/inc/pyc/bitcoin/service/BitStamp.scala @@ -0,0 +1,47 @@ +package inc.pyc.bitcoin +package service + +import dispatch._ +import akka.actor._ +import net.liftweb.json._ + +/** + * BitStamp REST services + * NOTE: Bitstamp is almost useless since it keeps asking for captcha -_- + */ +class BitStamp extends Actor + with ActorLogging + with HttpBitcoinExchange { + + import BitStamp._ + implicit val formats = DefaultFormats + + def receive = priceTicker + + protected val api = :/ ("www.bitstamp.net").secure / "api" + + protected val ticker_api = api / "ticker" / "" + + protected def buyPrice: String = { + (request(ticker_api).extract[BitStampPrices]).last + } +} + +object BitStamp { + /** + * Different prices returned by BitStamp's API. + * + * Note: Currently not configurable. 'last' is default. + * + * last - last BTC price (BitStamp's Current Price) + * high - last 24 hours price high + * low - last 24 hours price low + * vwap - last 24 hours volume weighted average price + * volume - last 24 hours volume + * bid - highest buy order + * ask - lowest sell order + */ + case class BitStampPrices( + last: String, high: String, low: String, vwap: String, volume: String, + bid: String, ask: String, timestamp: String) +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BitcoinExchange.scala b/src/main/scala/inc/pyc/bitcoin/service/BitcoinExchange.scala @@ -0,0 +1,19 @@ +package inc.pyc.bitcoin +package service + +import akka.actor._ + +/** + * Bitcoin Exchange service to buy and sell bitcoin + */ +sealed trait BitcoinExchange + + + +/** + * Bitcoin Exchange service to buy and sell bitcoin via HTTP + */ +trait HttpBitcoinExchange + extends HttpBitcoinService with HttpBitcoinPriceTicker { + this: Actor with ActorLogging => +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BitcoinPriceTicker.scala b/src/main/scala/inc/pyc/bitcoin/service/BitcoinPriceTicker.scala @@ -0,0 +1,32 @@ +package inc.pyc.bitcoin +package service + +import dispatch.Req +import akka.actor._ + +/** + * A Bitcoin service with buy/sell prices. + */ +sealed trait BitcoinPriceTicker + +trait HttpBitcoinPriceTicker extends HttpBitcoinService with BitcoinPriceTicker { + this: Actor with ActorLogging => + + import PriceTickerCommands._ + /** + * Price ticker API + */ + protected val ticker_api: Req + + /** + * Gets the buy price + */ + protected def buyPrice: String + + val priceTicker: Receive = { + case Tick => + val price = Price(buyPrice) + log.info("New Price: {}", price.format) + sender ! price + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BitcoinServiceActor.scala b/src/main/scala/inc/pyc/bitcoin/service/BitcoinServiceActor.scala @@ -0,0 +1,14 @@ +package inc.pyc.bitcoin +package service + +import akka.actor._ + +/** + * Any Bitcoin Service Actor must be this. + */ +trait BitcoinServiceActor { + this: Actor with ActorLogging => + + protected val serviceName: String = + this.getClass.getName.split("""\.""").last +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BitcoinWallet.scala b/src/main/scala/inc/pyc/bitcoin/service/BitcoinWallet.scala @@ -0,0 +1,219 @@ +package inc.pyc.bitcoin +package service + +import net.liftweb.json._ +import net.liftweb.util.Helpers.tryo +import JsonAST.JValue +import scala.collection._ +import scala.concurrent._, duration._ +import akka.util.Timeout +import com.typesafe.config.{Config, ConfigFactory} +import akka.actor._ +import JsonRPC._ +import BitcoinJsonRPC._ + +/** + * Actor to handle bitcoin wallet communications with JSON-RPC. + */ +sealed trait BitcoinWallet extends BitcoinServiceActor { + this: Actor with ActorLogging => + + /** Wallet configuration */ + protected val config: Config + protected val walletUri: String + protected val rpcUser: String + protected val rpcPass: String + protected val walletPass: String + + /** + * The wait time for a response. This timeout is also used to set + * the timeout for walletpassphrase command. + */ + protected implicit val timeout: Timeout = 5 seconds + + /** + * Checks wallet json response. Throws exception if + * response is invalid, else it logs the response. + */ + protected def checkWalletResponse(json: JsonResponse, method: String = "") { + json.either.left.map { + case err => + new RuntimeException("Wallet Command '"+method+"' Failed: " + err) + } + + json.either.right.map { + case r => + implicit val formats = DefaultFormats + log.info("\nWallet Command '{}' Success:\n{}", method, pretty(render(Extraction.decompose(r)))) + } + } +} + + +/** + * Actor to handle bitcoin wallet communications with JSON-RPC + * over Http. + */ +trait HttpBitcoinWallet extends HttpBitcoinService with BitcoinWallet { + this: Actor with ActorLogging => + + import dispatch._ + + val bitcoinWallet: Receive = { + case CreateRawTransaction(inputs, receivers) => + val json = JsonMessage.createRawTransaction(inputs, receivers) + sender ! requestExtract(json, _.extract[String]) + + case GetBalance => + val json = JsonMessage.getBalance + sender ! requestExtract(json, _.extract[String]) + + case GetNewAddress => + val json = JsonMessage.getNewAddress + sender ! requestExtract(json, _.extract[String]) + + case GetRawTransaction(transactionHash) => + val json = JsonMessage.getRawTransaction(transactionHash) + sender ! requestExtract(json, _.extract[RawTransaction]) + + case ListUnspentTransactions(minConfirmations, maxConfirmations) => + val json = JsonMessage.listUnspentTransactions(minConfirmations, maxConfirmations) + sender ! requestExtract(json, _.extract[List[UnspentTransaction]]) + + case SendRawTransaction(signedTransaction) => + val json = JsonMessage.sendRawTransaction(signedTransaction) + sender ! requestExtract(json, _.extract[String]) + + case SignRawTransaction(transaction) => + val json = JsonMessage.signRawTransaction(transaction) + sender ! requestExtract(json, _.extract[SignedTransaction]) + + case WalletPassPhrase(walletPass, timeout) => + val json = JsonMessage.walletPassPhrase(walletPass, timeout) + request(post(json)) // fire & forget + + case ValidateAddress(address) => + val json = JsonMessage.validateAddress(address) + sender ! requestExtract(json, _.extract[AddressValidation]) + } + + + private def post(msg: JValue): Req = + url(walletUri) <:< Map("Content-type" -> "application/json-rpc") << + compact(render(msg)) as_!(rpcUser, rpcPass) + + /* All-in-one func: makes request, extracts response, extracts data. */ + private def requestExtract[T](req: JsonRequest, extractor: JValue => T): T = { + val json = request(post(req)).extract[JsonResponse] + checkWalletResponse(json, req.method) + val result = json.result.getOrElse(JNull) + extractor(result) + } +} + + +/** + * Actor to handle bitcoin wallet communications with JSON-RPC + * over WebSocket. + */ +trait WsBitcoinWallet extends WsBitcoinService with BitcoinWallet { + this: Actor with ActorLogging => + + override val api = new java.net.URI(walletUri) + + /** + * Handles notifications incoming from server. + */ + protected def handleNotification: Receive + + + val bitcoinWallet: Receive = { + case CreateRawTransaction(inputs, receivers) => + val json = JsonMessage.createRawTransaction(inputs, receivers) + requestExtract(json.id, json, _.extract[String], "CreateRawTransaction" :: Nil) + + case GetBalance => + val json = JsonMessage.getBalance + requestExtract(json.id, json, _.extract[String], "GetBalance" :: Nil) + + case GetNewAddress => + val json = JsonMessage.getNewAddress + requestExtract(json.id, json, _.extract[String], "GetNewAddress" :: Nil) + + case GetRawTransaction(transactionHash) => + val json = JsonMessage.getRawTransaction(transactionHash) + requestExtract(json.id, json, _.extract[RawTransaction], "GetRawTransaction" :: Nil) + + case ListUnspentTransactions(minConfirmations, maxConfirmations) => + val json = JsonMessage.listUnspentTransactions(minConfirmations, maxConfirmations) + requestExtract(json.id, json, _.extract[List[UnspentTransaction]], "ListUnspentTransactions" :: Nil) + + case SendRawTransaction(signedTransaction) => + val json = JsonMessage.sendRawTransaction(signedTransaction) + requestExtract(json.id, json, _.extract[String], "SendRawTransaction" :: Nil) + + case SignRawTransaction(transaction) => + val json = JsonMessage.signRawTransaction(transaction) + requestExtract(json.id, json, _.extract[SignedTransaction], "SignRawTransaction" :: Nil) + + case WalletPassPhrase(walletPass, timeout) => + val json = JsonMessage.walletPassPhrase(walletPass, timeout) + requestForget(json) + + case ValidateAddress(address) => + val json = JsonMessage.validateAddress(address) + requestExtract(json.id, json, _.extract[AddressValidation], "ValidateAddress" :: Nil) + + case json @ JsonResponse(jsonrpc, id, errorOption, resultOption) => + requests.remove(id).foreach(req => { + val (p, extract, info) = req + checkWalletResponse(json, info.head) + json.either.left.map(p tryFailure new RuntimeException(_)) + json.either.right.map(p trySuccess extract(_)) + }) + + case n: NotificationMessage => + handleNotification.applyOrElse(n, unhandled) + } + + /* + * Handles notifications incoming from server. + */ + private def handleResponseNotification: PartialFunction[JsonNotification, Unit] = { + + /* New Transaction */ + case JsonNotification(_, "newtx", params) => + for { + tx <- tryo(params(1).extract[TransactionNotification]) + if tx.category == "receive" + } self ! ReceivedPayment(tx.txid, tx.address, tx.amount, tx.confirmations) + + case _ => // ignore + } + + override def onMessage(msg: JValue) = () => { + + // messages should be either JsonNotification or JsonResponse + // only notifications have jsonrpc field. + def isNotification: Boolean = + (msg find { + case JField("jsonrpc", _) => true + case _ => false + }).isDefined + + if(isNotification) + tryo(msg.extract[JsonNotification]).map( + handleResponseNotification.applyOrElse(_, unhandled)) + else + tryo(msg.extract[JsonResponse]).map(self ! _) + } +} + + +/** + * Actor to handle bitcoin wallet communications with JSON-RPC + * over Secure WebSocket. + */ +trait WssBitcoinWallet extends WssBitcoinService with WsBitcoinWallet { + this: Actor with ActorLogging => +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BlockChain.scala b/src/main/scala/inc/pyc/bitcoin/service/BlockChain.scala @@ -0,0 +1,34 @@ +package inc.pyc.bitcoin +package service + +import dispatch._ +import net.liftweb.json._ +import akka.actor._ + +/** + * BlockChain REST services + */ +class BlockChain extends Actor + with ActorLogging + with HttpBitcoinPriceTicker + with HttpBitcoinWallet { + + this: Actor with ActorLogging => + + def receive = bitcoinWallet orElse priceTicker + + protected val config = BitcoinSystem.config.getConfig("blockchain") + protected val walletUri = config.getString("wallet-uri") + protected val rpcUser = config.getString("rpc-user") + protected val rpcPass = config.getString("rpc-pass") + protected val walletPass = config.getString("wallet-pass") + + protected val api = :/ ("blockchain.info").secure + protected val ticker_api = api / "ticker" + + protected def buyPrice: String = + for (JField("USD", usd) <- request(ticker_api)) + yield compact(render(usd \ "last")) + + implicit def listStringToString(s: List[String]): String = s.head +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/BtcWallet.scala b/src/main/scala/inc/pyc/bitcoin/service/BtcWallet.scala @@ -0,0 +1,25 @@ +package inc.pyc.bitcoin +package service + +import net.liftweb.json._ +import akka.actor._ + +class BtcWallet extends Actor + with ActorLogging + with WssBitcoinWallet { + + val config = BitcoinSystem.config.getConfig("btcwallet") + val walletUri = config.getString("wallet-uri") + val rpcUser = config.getString("rpc-user") + val rpcPass = config.getString("rpc-pass") + val walletPass = config.getString("wallet-pass") + val keyStoreFile = new java.io.File(config.getString("keystore-file")) + val keyStorePass = config.getString("keystore-pass") + + def receive = bitcoinWallet orElse websocket + + def handleNotification = { + case _ => + } + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/HttpBitcoinService.scala b/src/main/scala/inc/pyc/bitcoin/service/HttpBitcoinService.scala @@ -0,0 +1,36 @@ +package inc.pyc.bitcoin +package service + +import dispatch._ +import net.liftweb.json.JsonAST.JValue +import akka.actor._ + +/** + * Bitcoin service over HTTP communications. + */ +trait HttpBitcoinService extends BitcoinServiceActor { + this: Actor with ActorLogging => + + /** + * Service's API + */ + protected val api: Req + + /** + * Request headers + */ + protected val headers: Map[String, String] = Map() + + /** + * Pretend to be another browser. + */ + protected val userAgent = "Windows / Chrome 34: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/537.36" + + /** + * Request and get a json response. + */ + protected def request(req: Req): JValue = ({ + import Defaults._ + Http.configure(_.setUserAgent(userAgent))(req <:< headers > as.lift.Json): Future[JValue] + })() +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/JsonRPC.scala b/src/main/scala/inc/pyc/bitcoin/service/JsonRPC.scala @@ -0,0 +1,135 @@ +package inc.pyc.bitcoin +package service + +import net.liftweb._ +import json.JsonAST._ + +sealed trait JsonMessage + +/** + * For more information, visit + * http://json-rpc.org/wiki/specification + */ +object JsonRPC { + implicit val formats = json.DefaultFormats + + // JSON-RPC MESSAGES + case class JsonNotification(jsonrpc: String, method: String, params: JArray) extends JsonMessage + case class JsonRequest(jsonrpc: String, id: String, method: String, params: JArray) extends JsonMessage + case class JsonResponse(jsonrpc: String, id: String, error: Option[JValue], result: Option[JValue]) extends JsonMessage { + def either: Either[String, JValue] = + (result, error) match { + case (Some(result), _) => Right(result) + case (_, Some(error)) => Left((error \ "message").extract[String]) + case _ => Left("Unknown response") + } + } + + // JsonMessage to JValue implicit + implicit def jsonRequestToJValue(r: JsonRequest): JValue = { + JObject(List( + JField("jsonrpc", JString(r.jsonrpc)), + JField("id", JString(r.id)), + JField("method", JString(r.method)), + JField("params", r.params))) + } +} + +trait WalletMessage +trait NotificationMessage extends WalletMessage + +/** + * For more information, visit + * https://en.bitcoin.it/wiki/API_reference_(JSON-RPC) + */ +object BitcoinJsonRPC { + import JsonRPC._ + + // BITCOIN TRANSACTIONS + case class RawTransaction(hex: String, txid: String, version: BigDecimal, locktime: BigDecimal, vin: Seq[VIn], vout: Seq[VOut]) + case class VIn(txid: String, vout: Int, scriptSig: ScriptSig, sequence: BigDecimal) + case class VOut(value: BigDecimal, n: BigDecimal, scriptPubKey: ScriptPubKey) + case class ScriptSig(asm: String, hex: String) + case class ScriptPubKey(asm: String, hex: String, reqSigs: BigDecimal, `type`: String, addresses: Seq[String]) + case class UnspentTransaction(txid: String, account: String, address: String, amount: BigDecimal, confirmations: BigDecimal) + case class SignedTransaction(hex: String, complete: Boolean) + case class TransactionNotification(txid: String, account: String, address: String, + category: String, amount: BigDecimal, confirmations: BigDecimal, timereceived: BigDecimal) + + // OTHER BITCOIN MESSAGES + case class AddressValidation(isvalid: Boolean, address: String, + ismine: Option[Boolean], pubkey: Option[String], iscompressed: Option[Boolean]) + + // ACTOR MESSAGES + // notifications + case class ReceivedPayment(txId: String, address: String, amount: BigDecimal, confirmations: BigDecimal) extends NotificationMessage + + // requests + sealed trait RequestMessage extends WalletMessage + case class CreateRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) extends RequestMessage + case object GetBalance extends RequestMessage + case object GetNewAddress extends RequestMessage + case class GetRawTransaction(transactionHash: String) extends RequestMessage + case class ListUnspentTransactions(minConfirmations: BigDecimal = 1, maxConfirmations: BigDecimal = 999999) extends RequestMessage + case class SendRawTransaction(signedTransaction: String) extends RequestMessage + case class SignRawTransaction(transaction: String) extends RequestMessage + case class WalletPassPhrase(walletPass: String, timeout: BigDecimal) extends RequestMessage + case class ValidateAddress(address: String) extends RequestMessage + + + /** + * Constructs JSON-RPC messages out of the existing actor messages related + * to Bitcoin's standard rpc-json commands. + */ + object JsonMessage { + def createRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) = + JsonRequest("1.0", Utils.getUUID, "createrawtransaction", JArray( + inputs.map(i => JObject(List("txid" -> i._1, "vout" -> i._2))).toList :: + receivers.map(r => JObject(List(r._1 -> r._2))).toList)) + + def getBalance = + JsonRequest("1.0", Utils.getUUID, "getbalance", Seq()) + + def getNewAddress = + JsonRequest("1.0", Utils.getUUID, "getnewaddress", Seq()) + + def getRawTransaction(transactionHash: String) = + JsonRequest("1.0", Utils.getUUID, "getrawtransaction", JArray(List(transactionHash, 1))) + + def listUnspentTransactions(minConfirmations: BigDecimal, maxConfirmations: BigDecimal, addresses: Seq[String] = Seq.empty[String]) = { + val params = + if (addresses.isEmpty) JArray(List(minConfirmations, maxConfirmations)) + else JArray(List(minConfirmations, maxConfirmations, addresses)) + JsonRequest("1.0", Utils.getUUID, "listunspent", params) + } + + def sendRawTransaction(signedTransaction: String) = + JsonRequest("1.0", Utils.getUUID, "sendrawtransaction", Seq(signedTransaction)) + + def signRawTransaction(transaction: String) = + JsonRequest("1.0", Utils.getUUID, "signrawtransaction", Seq(transaction)) + + def walletPassPhrase(walletPass: String, timeout: BigDecimal) = + JsonRequest("1.0", Utils.getUUID, "walletpassphrase", JArray(List(walletPass, timeout))) + + def validateAddress(address: String) = + JsonRequest("1.0", Utils.getUUID, "validateaddress", Seq(address)) + } + + private object Utils { + def getUUID = java.util.UUID.randomUUID.toString + } + + // JValue implicits + implicit def intToJInt(i: Int): JInt = JInt(i) + implicit def stringToJString(s: String): JString = JString(s) + implicit def stringToJField(m: (String, String)): JField = JField(m._1, m._2) + + // BigDecimal to JValue implicits + implicit def bigdecimalToJField(m: (String, BigDecimal)): JField = JField(m._1, m._2) + implicit def bigdecimalToJDouble(d: BigDecimal): JDouble = JDouble(d.doubleValue) + + // Scala collections to JArray implicits + implicit def listToJArray(l: List[JValue]): JArray = JArray(l) + implicit def seqStringToJArray(l: Seq[String]): JArray = JArray(l.map(JString).toList) +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/WsBitcoinService.scala b/src/main/scala/inc/pyc/bitcoin/service/WsBitcoinService.scala @@ -0,0 +1,200 @@ +package inc.pyc.bitcoin +package service + +import java.net.{URI, ConnectException} +import concurrent._ +import duration._ +import collection._ +import collection.JavaConversions._ +import scala.util.{Success, Failure} +import akka.actor._ +import akka.pattern._ +import akka.util._ +import net.liftweb.json._ +import JsonAST.JValue +import org.java_websocket._ +import client._ +import handshake._ +import drafts._ + + +/** + * Bitcoin service over WebSocket communications. + */ +trait WsBitcoinService extends BitcoinServiceActor { + this: Actor with ActorLogging => + + import context.dispatcher + + protected implicit val timeout: Timeout + + /** + * Service's API + */ + protected val api: URI + + /** + * Handle json messages sent from server. + */ + protected def onMessage(msg: JValue): () => Unit + + /** + * Request headers + */ + protected val headers: Map[String,String] = Map() + + /** + * Maps request IDs to the corresponding response promises + * and a function that converts the JSON RPC response to the final actor response. + */ + protected val requests = mutable.HashMap.empty[String, (Promise[Any], JValue => _, List[String])] + + /** + * Executes on websocket initial connection. + */ + protected def onConnect: () => Unit = () => {} + + /** + * Request and forget. + */ + protected def requestForget(req: JValue): Unit = { + request(req) + } + + /** + * Request and extract promised json response. + * Note: Must be an actor making the request to receive response. + */ + protected def requestExtract[T](id: String, req: JValue, extractor: JValue => T, info: List[String] = Nil): Unit = { + + val p = Promise[Any]() + val f = p.future + requests += id -> (p, extractor, info) + request(req) + + context.system.scheduler.scheduleOnce(timeout.duration) { + p tryFailure { + new TimeoutException(s"Timeout: $serviceName bitcoin wallet did not respond in time") + } + self ! RemoveWsRequest(id) + } + + pipe(f) to sender + } + + override def preStart(): Unit = { + context.become(connecting) + tryToConnect() + } + + /** + * Receive when disconnected. + */ + def connecting: Receive = { + case Connected => + context.become(receive) + onConnect() + + case _ => + val message = s"Cannot process request: no connection to $serviceName websocket." + sender ! Status.Failure(new IllegalStateException(message)) + log.error(message) + } + + /** + * Receive when connected. + */ + val websocket: Receive = { + case RemoveWsRequest(id) => + requests -= id + + case Disconnected(reason) => + log.warning("Connection to {} bitcoin wallet closed: {}", serviceName, reason) + context.become(connecting) + tryToConnect() + } + + /** + * Tries to connect to the websocket. + * @param f function to manipulate the websocket object before trying to connect. + */ + protected def tryToConnect(f: WebSocketBtcWalletClient => WebSocketBtcWalletClient = walletClient => walletClient) { + requests.clear() + walletClient = new WebSocketBtcWalletClient(api, new Draft_17, headers, 0) + walletClient = f(walletClient) + val connected = walletClient.connectBlocking() + + if (connected) { + log.info("Connection to {} bitcoin wallet established", serviceName) + self ! Connected + } else { + new ConnectException(s"$serviceName bitcoin wallet not available: $api") + } + } + + /* Send request down websocket. */ + private def request(req: JValue) { walletClient.send(compact(render(req))) } + + /* Personal websocket class of the java-websocket library. */ + class WebSocketBtcWalletClient(serverUri: URI, protocolDraft: Draft, httpHeaders: Map[String, String], connectTimeout: Int) + extends WebSocketClient(serverUri, protocolDraft, headers, connectTimeout) { + override def onMessage(jsonMessage: String): Unit = { ws.onMessage(parse(jsonMessage)) } + override def onOpen(handshakeData: ServerHandshake) { } + override def onClose(code: Int, reason: String, remote: Boolean) { self ! Disconnected(reason) } + override def onError(ex: Exception) { self ! Disconnected(ex.getMessage()) } + } + + /* The actual websocket. */ + protected var walletClient: WebSocketBtcWalletClient = null + + /* Used by the websocket class to access actor methods with same name. */ + private def ws = this + + /* Websocket Messages */ + case object Connected + case class Disconnected(reason: String) + case class RemoveWsRequest(id: String) +} + +/** + * Bitcoin service over WebSocket communications with SSL. + */ +trait WssBitcoinService extends WsBitcoinService { + this: Actor with ActorLogging => + + import java.io._ + import java.security._ + import javax.net.ssl._ + + /** + * KeyStore file containing trusted certificates. + */ + val keyStoreFile: File + + /** + * Password of the KeyStore `keyStoreFile` + */ + val keyStorePass: String + + private val socketFactory = createSslSocketFactory + + override def tryToConnect(f: WebSocketBtcWalletClient => WebSocketBtcWalletClient = walletClient => walletClient) = + super.tryToConnect( walletClient => { + walletClient.setSocket(socketFactory.createSocket()) + walletClient + }) + + /** + * Creates a ssl socket factory for the websocket using the + * given keystore file and password. + */ + private def createSslSocketFactory: SSLSocketFactory = { + val ks = KeyStore.getInstance("JKS") + ks.load(new FileInputStream(keyStoreFile), keyStorePass.toCharArray) + val tmf = TrustManagerFactory.getInstance("SunX509") + tmf.init(ks) + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, tmf.getTrustManagers, null) + sslContext.getSocketFactory + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/BitcoinServices.scala b/src/main/scala/inc/pyc/chimera/bitcoin/BitcoinServices.scala @@ -1,70 +0,0 @@ -package inc.pyc.chimera -package bitcoin - -import lycia._ -import service._ -import akka.actor.{ActorSystem, Props} -import inc.pyc.chimera.snippet.LiftActorEventBus - -/** - * The different choices for bitcoin services. - */ -object BitcoinService extends Enumeration { - type BitcoinService = Value - val BitStamp, BlockChain, BtcWallet = Value - - def priceTickers: Map[BitcoinService, Props] = Map( - BitStamp -> Props[BitStamp], - BlockChain -> Props[BlockChain] - ) - - def wallets: Map[BitcoinService, Props] = Map( - BtcWallet -> Props[BtcWallet], - BlockChain -> Props[BlockChain] - ) - - - def getPriceTicker(s: Value): Props = { - get(s, priceTickers) - } - - def getWallet(s: Value): Props = { - get(s, wallets) - } - - private def get(s: Value, m: Map[BitcoinService, Props]) = { - m.filter(_._1 == s).map(_._2).head - } -} - - -/** - * Command to change the bitcoin service provider. - */ -case class ChangeBitcoinService(service: BitcoinService.Value) - - -/** - * Main bitcoin actor services - */ -object BitcoinServices { - - /** - * Bitcoin services Actor System - */ - val system = ActorSystem("BitcoinServicesSystem") - - /** - * Event Bus for Bitcoin services. - */ - val bus = new LookupBitcoinServices -} - - -/** - * PubSub for Bitcoin services. - */ -class LookupBitcoinServices extends LiftActorEventBus { - // newPrice - override protected def mapSize: Int = 10 -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/PriceTicker.scala b/src/main/scala/inc/pyc/chimera/bitcoin/PriceTicker.scala @@ -1,143 +0,0 @@ -package inc.pyc.chimera -package bitcoin - -import lycia._ -import service._ -import snippet.EventUpdate -import akka.actor._ -import akka.event._ -import akka.util.Timeout -import SupervisorStrategy._ -import akka.pattern.{ask, pipe} -import scala.concurrent._ -import duration._ -import BitcoinServices.system.dispatcher - - -object PriceTicker { - import BitcoinServices._ - import commands._ - - implicit val timeout: Timeout = Timeout(5 seconds) - - def !(any: Any): Unit = actor ! any - - /** - * The main actor that handles communications. - */ - private val actor = system.actorOf(Props[PriceTicker], "PriceTicker") - - /** - * The ticker that updates the price every 5 minutes. - */ - private val ticker = - system.scheduler.schedule(0 milliseconds, 5 minutes, actor, Tick) - - /** - * Subscription topics for event bus. - */ - object topics { - val newPrice = "newPrice" - } - - object commands { - /** - * Price per bitcoin. - */ - case class Price(price: String) { - def format: String = "%1.2f" format (price.toDouble) - def double: Double = price.toDouble - } - - /** - * Command to send a broadcast of the buy price. - * Comet will be listening to update the UI. - */ - case object GossipPrice - - /** - * Command to send a unicast of the buy price. - */ - case object GetPrice - - /** - * Command to set a percentage price over market. - * If price over market is 5%, send 5, not 0.05, as the profit value. - */ - case class Percentage(profit: Double) - - /** - * Command to update the price per bitcoin. - * Sent frequently to update the price in the UI. - */ - case object Tick - } - - def price(): Future[Price] = (actor ? GetPrice).mapTo[Price] -} - -/** - * Main Price Ticker actor that creates child actor and uses the chosen, - * configured service to get the price per bitcoin. - */ -class PriceTicker extends Actor with ActorLogging { - import BitcoinServices._ - import BitcoinService._ - import PriceTicker._, commands._ - - - override val supervisorStrategy = - OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { - case _: java.net.ConnectException => Resume - case t => - super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate) - } - - /** The current service to use to check price. */ - private var service: ActorRef = createServiceActor(Lycia.servicePriceTicker) - - context.watch(service) - - /** Price per bitcoin */ - private var price: String = "0.00" - - /** Percentage price over market */ - private var percentage: Double = 0 - - def receive = { - case Tick => - service ! Tick - - case GetPrice => - sender ! Price(priceWithPercentage) - - case GossipPrice => - self ! Price(priceWithPercentage) - - case Percentage(profit) => - percentage = profit - - case Price(newPrice) => - price = newPrice - bus.publish(EventUpdate(topics.newPrice, Price(price))) - - case ChangeBitcoinService(provider) => - context.unwatch(service) - context.stop(service) - service = createServiceActor(provider) - context.watch(service) - - case _ => - log.warning("Received Unknown Message") - } - - private def priceWithPercentage: String = { - val p = price.toDouble - val withPercentage = p + (p * (percentage * 0.01)) - Price(withPercentage toString) format - } - - private def createServiceActor(service: BitcoinService.Value): ActorRef = { - context.actorOf(getPriceTicker(service), service.toString) - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/Wallet.scala b/src/main/scala/inc/pyc/chimera/bitcoin/Wallet.scala @@ -1,157 +0,0 @@ -package inc.pyc.chimera -package bitcoin - -import lycia._ -import service.BitcoinJsonRPC -import snippet.EventUpdate -import akka.actor._ -import akka.event._ -import akka.util._ -import SupervisorStrategy._ -import akka.pattern.{ask, pipe} -import BitcoinServices.system.dispatcher -import scala.concurrent._, duration._ -import net.liftweb.util.Helpers.tryo -import BitcoinJsonRPC._ - -object Wallet { - import BitcoinServices._ - - implicit val timeout: Timeout = Timeout(5 seconds) - - def !(any: Any): Unit = actor ! any - def ?(any: Any): Future[Any] = actor ? any - - /** - * The main actor that handles communications. - */ - private val actor = system.actorOf(Props[Wallet], "BitcoinWallet") - - /** - * Subscription topics for event bus. - */ - object topics { - val newBalance = "newBalance" - } - - object commands { - /** - * Balance remaining in bitcoin wallet. - */ - case class Balance(remaining: Double) - - /** - * Command to send a broadcast of the balance. - */ - case object GossipBalance - - /** - * Initializes the balance in wallet - */ - case object InitBalance - } - - import commands._ - - def validateAddress(address: String): Future[AddressValidation] = - (actor ? ValidateAddress(address)).mapTo[AddressValidation] -} - -/** - * Main Bitcoin Wallet actor that creates child actor and uses the chosen, - * configured service to get the price per bitcoin. - */ -class Wallet extends Actor with ActorLogging { - import BitcoinServices._ - import BitcoinService._ - import Wallet._, commands._ - - - override val supervisorStrategy = - OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { - case _: java.net.ConnectException => Resume - case t => - super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate) - } - - /** The current service to use to check price. */ - private var service: ActorRef = createServiceActor(Lycia.serviceWallet) - - context.watch(service) - - /** Balance remaining in the wallet */ - private var balance: Double = 0 - - def receive = { - case InitBalance => - getBalance().map(newBalance => tryo { - balance = newBalance.toDouble - }) - - case GetBalance => - sender ! Balance(balance) - - case GossipBalance => - self ! Balance(balance) - - case Balance(newBalance) => - balance = newBalance - bus.publish(EventUpdate(topics.newBalance, Balance(balance))) - - case ChangeBitcoinService(provider) => - context.unwatch(service) - context.stop(service) - service = createServiceActor(provider) - context.watch(service) - - case ValidateAddress(address) => - validateAddress(extractFromBitcoinUri(address)) pipeTo sender - - - case _ => - log.warning("Received Unknown Message") - } - - private def createServiceActor(service: BitcoinService.Value): ActorRef = { - context.actorOf(getWallet(service), service.toString) - } - - /** - * Extracts bitcoin address from URI. - * - * Note the regex is not what the bitcoin address should exactly be, - * but it works for extraction. - */ - private def extractFromBitcoinUri(uri: String) = { - val r = "(bitcoin:)?([a-zA-Z0-9]{1,60})(/*?.*)?".r - uri match { - case r(_, address, _) => address - case _ => "" - } - } - - - private def createRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) = - (service ? CreateRawTransaction(inputs, receivers)).mapTo[String] - - private def listUnspentTransactions(minConfirmations: BigDecimal = 1, maxConfirmations: BigDecimal = 999999) = - (service ? ListUnspentTransactions(minConfirmations, maxConfirmations)).mapTo[Seq[UnspentTransaction]] - - private def sendRawTransaction(signedTransaction: String) = - (service ? SendRawTransaction(signedTransaction)).mapTo[String] - - def getBalance(): Future[String] = - (service ? GetBalance).mapTo[String] - - private def getNewAddress(): Future[String] = - (service ? GetNewAddress).mapTo[String] - - def getRawTransaction(transactionHash: String) = - (service ? GetRawTransaction(transactionHash)).mapTo[RawTransaction] - - def signRawTransaction(transaction: String): Future[SignedTransaction] = - (service ? SignRawTransaction(transaction)).mapTo[SignedTransaction] - - def validateAddress(address: String): Future[AddressValidation] = - (service ? ValidateAddress(address)).mapTo[AddressValidation] -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BitStamp.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BitStamp.scala @@ -1,48 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import dispatch._ -import akka.actor._ -import net.liftweb.json._ - -/** - * BitStamp REST services - * NOTE: Bitstamp is almost useless since it keeps asking for captcha -_- - */ -class BitStamp extends Actor - with ActorLogging - with HttpBitcoinExchange { - - import BitStamp._ - implicit val formats = DefaultFormats - - def receive = priceTicker - - protected val api = :/ ("www.bitstamp.net").secure / "api" - - protected val ticker_api = api / "ticker" / "" - - protected def buyPrice: String = { - (request(ticker_api).extract[BitStampPrices]).last - } -} - -object BitStamp { - /** - * Different prices returned by BitStamp's API. - * - * Note: Currently not configurable. 'last' is default. - * - * last - last BTC price (BitStamp's Current Price) - * high - last 24 hours price high - * low - last 24 hours price low - * vwap - last 24 hours volume weighted average price - * volume - last 24 hours volume - * bid - highest buy order - * ask - lowest sell order - */ - case class BitStampPrices( - last: String, high: String, low: String, vwap: String, volume: String, - bid: String, ask: String, timestamp: String) -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinExchange.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinExchange.scala @@ -1,20 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import akka.actor._ - -/** - * Bitcoin Exchange service to buy and sell bitcoin - */ -sealed trait BitcoinExchange - - - -/** - * Bitcoin Exchange service to buy and sell bitcoin via HTTP - */ -trait HttpBitcoinExchange - extends HttpBitcoinService with HttpBitcoinPriceTicker { - this: Actor with ActorLogging => -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinPriceTicker.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinPriceTicker.scala @@ -1,33 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import dispatch.Req -import akka.actor._ - -/** - * A Bitcoin service with buy/sell prices. - */ -sealed trait BitcoinPriceTicker - -trait HttpBitcoinPriceTicker extends HttpBitcoinService with BitcoinPriceTicker { - this: Actor with ActorLogging => - - import PriceTicker.commands._ - /** - * Price ticker API - */ - protected val ticker_api: Req - - /** - * Gets the buy price - */ - protected def buyPrice: String - - val priceTicker: Receive = { - case Tick => - val price = Price(buyPrice) - log.info("New Price: {}", price.format) - sender ! price - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinServiceActor.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinServiceActor.scala @@ -1,15 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import akka.actor._ - -/** - * Any Bitcoin Service Actor must be this. - */ -trait BitcoinServiceActor { - this: Actor with ActorLogging => - - protected val serviceName: String = - this.getClass.getName.split("""\.""").last -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinWallet.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinWallet.scala @@ -1,221 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import net.liftweb.json._ -import net.liftweb.util.Helpers.tryo -import JsonAST.JValue -import scala.collection._ -import scala.concurrent._, duration._ -import BitcoinServices.system.dispatcher -import akka.util.Timeout -import com.typesafe.config.{Config, ConfigFactory} -import akka.actor._ -import JsonRPC._ -import BitcoinJsonRPC._ - -/** - * Actor to handle bitcoin wallet communications with JSON-RPC. - */ -sealed trait BitcoinWallet extends BitcoinServiceActor { - this: Actor with ActorLogging => - - /** Wallet configuration */ - protected val config: Config - protected val walletUri: String - protected val rpcUser: String - protected val rpcPass: String - protected val walletPass: String - - /** - * The wait time for a response. This timeout is also used to set - * the timeout for walletpassphrase command. - */ - protected implicit val timeout: Timeout = 5 seconds - - /** - * Checks wallet json response. Throws exception if - * response is invalid, else it logs the response. - */ - protected def checkWalletResponse(json: JsonResponse, method: String = "") { - json.either.left.map { - case err => - new RuntimeException("Wallet Command '"+method+"' Failed: " + err) - } - - json.either.right.map { - case r => - implicit val formats = DefaultFormats - log.info("\nWallet Command '{}' Success:\n{}", method, pretty(render(Extraction.decompose(r)))) - } - } -} - - -/** - * Actor to handle bitcoin wallet communications with JSON-RPC - * over Http. - */ -trait HttpBitcoinWallet extends HttpBitcoinService with BitcoinWallet { - this: Actor with ActorLogging => - - import dispatch._ - - val bitcoinWallet: Receive = { - case CreateRawTransaction(inputs, receivers) => - val json = JsonMessage.createRawTransaction(inputs, receivers) - sender ! requestExtract(json, _.extract[String]) - - case GetBalance => - val json = JsonMessage.getBalance - sender ! requestExtract(json, _.extract[String]) - - case GetNewAddress => - val json = JsonMessage.getNewAddress - sender ! requestExtract(json, _.extract[String]) - - case GetRawTransaction(transactionHash) => - val json = JsonMessage.getRawTransaction(transactionHash) - sender ! requestExtract(json, _.extract[RawTransaction]) - - case ListUnspentTransactions(minConfirmations, maxConfirmations) => - val json = JsonMessage.listUnspentTransactions(minConfirmations, maxConfirmations) - sender ! requestExtract(json, _.extract[List[UnspentTransaction]]) - - case SendRawTransaction(signedTransaction) => - val json = JsonMessage.sendRawTransaction(signedTransaction) - sender ! requestExtract(json, _.extract[String]) - - case SignRawTransaction(transaction) => - val json = JsonMessage.signRawTransaction(transaction) - sender ! requestExtract(json, _.extract[SignedTransaction]) - - case WalletPassPhrase(walletPass, timeout) => - val json = JsonMessage.walletPassPhrase(walletPass, timeout) - request(post(json)) // fire & forget - - case ValidateAddress(address) => - val json = JsonMessage.validateAddress(address) - sender ! requestExtract(json, _.extract[AddressValidation]) - } - - - private def post(msg: JValue): Req = - url(walletUri) <:< Map("Content-type" -> "application/json-rpc") << - compact(render(msg)) as_!(rpcUser, rpcPass) - - /* All-in-one func: makes request, extracts response, extracts data. */ - private def requestExtract[T](req: JsonRequest, extractor: JValue => T): T = { - val json = request(post(req)).extract[JsonResponse] - checkWalletResponse(json, req.method) - val result = json.result.getOrElse(JNull) - extractor(result) - } -} - - -/** - * Actor to handle bitcoin wallet communications with JSON-RPC - * over WebSocket. - */ -trait WsBitcoinWallet extends WsBitcoinService with BitcoinWallet { - this: Actor with ActorLogging => - - override val api = new java.net.URI(walletUri) - - /** - * Handles notifications incoming from server. - */ - protected def handleNotification: Receive - - - val bitcoinWallet: Receive = { - case CreateRawTransaction(inputs, receivers) => - val json = JsonMessage.createRawTransaction(inputs, receivers) - requestExtract(json.id, json, _.extract[String], "CreateRawTransaction" :: Nil) - - case GetBalance => - val json = JsonMessage.getBalance - requestExtract(json.id, json, _.extract[String], "GetBalance" :: Nil) - - case GetNewAddress => - val json = JsonMessage.getNewAddress - requestExtract(json.id, json, _.extract[String], "GetNewAddress" :: Nil) - - case GetRawTransaction(transactionHash) => - val json = JsonMessage.getRawTransaction(transactionHash) - requestExtract(json.id, json, _.extract[RawTransaction], "GetRawTransaction" :: Nil) - - case ListUnspentTransactions(minConfirmations, maxConfirmations) => - val json = JsonMessage.listUnspentTransactions(minConfirmations, maxConfirmations) - requestExtract(json.id, json, _.extract[List[UnspentTransaction]], "ListUnspentTransactions" :: Nil) - - case SendRawTransaction(signedTransaction) => - val json = JsonMessage.sendRawTransaction(signedTransaction) - requestExtract(json.id, json, _.extract[String], "SendRawTransaction" :: Nil) - - case SignRawTransaction(transaction) => - val json = JsonMessage.signRawTransaction(transaction) - requestExtract(json.id, json, _.extract[SignedTransaction], "SignRawTransaction" :: Nil) - - case WalletPassPhrase(walletPass, timeout) => - val json = JsonMessage.walletPassPhrase(walletPass, timeout) - requestForget(json) - - case ValidateAddress(address) => - val json = JsonMessage.validateAddress(address) - requestExtract(json.id, json, _.extract[AddressValidation], "ValidateAddress" :: Nil) - - case json @ JsonResponse(jsonrpc, id, errorOption, resultOption) => - requests.remove(id).foreach(req => { - val (p, extract, info) = req - checkWalletResponse(json, info.head) - json.either.left.map(p tryFailure new RuntimeException(_)) - json.either.right.map(p trySuccess extract(_)) - }) - - case n: NotificationMessage => - handleNotification.applyOrElse(n, unhandled) - } - - /* - * Handles notifications incoming from server. - */ - private def handleResponseNotification: PartialFunction[JsonNotification, Unit] = { - - /* New Transaction */ - case JsonNotification(_, "newtx", params) => - for { - tx <- tryo(params(1).extract[TransactionNotification]) - if tx.category == "receive" - } self ! ReceivedPayment(tx.txid, tx.address, tx.amount, tx.confirmations) - - case _ => // ignore - } - - override def onMessage(msg: JValue) = () => { - - // messages should be either JsonNotification or JsonResponse - // only notifications have jsonrpc field. - def isNotification: Boolean = - (msg find { - case JField("jsonrpc", _) => true - case _ => false - }).isDefined - - if(isNotification) - tryo(msg.extract[JsonNotification]).map( - handleResponseNotification.applyOrElse(_, unhandled)) - else - tryo(msg.extract[JsonResponse]).map(self ! _) - } -} - - -/** - * Actor to handle bitcoin wallet communications with JSON-RPC - * over Secure WebSocket. - */ -trait WssBitcoinWallet extends WssBitcoinService with WsBitcoinWallet { - this: Actor with ActorLogging => -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BlockChain.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BlockChain.scala @@ -1,36 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import dispatch._ -import net.liftweb.json._ -import akka.actor._ -import com.typesafe.config.{Config, ConfigFactory} - -/** - * BlockChain REST services - */ -class BlockChain extends Actor - with ActorLogging - with HttpBitcoinPriceTicker - with HttpBitcoinWallet { - - this: Actor with ActorLogging => - - def receive = bitcoinWallet orElse priceTicker - - protected val config: Config = ConfigFactory.load().getConfig("blockchain") - protected val walletUri: String = config.getString("wallet-uri") - protected val rpcUser: String = config.getString("rpc-user") - protected val rpcPass: String = config.getString("rpc-pass") - protected val walletPass: String = config.getString("wallet-pass") - - protected val api = :/ ("blockchain.info") - protected val ticker_api = api / "ticker" - - protected def buyPrice: String = - for (JField("USD", usd) <- request(ticker_api)) - yield compact(render(usd \ "last")) - - implicit def listStringToString(s: List[String]): String = s.head -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BtcWallet.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BtcWallet.scala @@ -1,27 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import net.liftweb.json._ -import akka.actor._ -import com.typesafe.config.{Config, ConfigFactory} - -class BtcWallet extends Actor - with ActorLogging - with WssBitcoinWallet { - - val config: Config = ConfigFactory.load().getConfig("btcwallet") - val walletUri: String = config.getString("wallet-uri") - val rpcUser: String = config.getString("rpc-user") - val rpcPass: String = config.getString("rpc-pass") - val walletPass: String = config.getString("wallet-pass") - val keyStoreFile = new java.io.File(config.getString("keystore-file")) - val keyStorePass = config.getString("keystore-pass") - - def receive = bitcoinWallet orElse websocket - - def handleNotification = { - case _ => - } - -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/HttpBitcoinService.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/HttpBitcoinService.scala @@ -1,36 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import dispatch._, Defaults._ -import net.liftweb.json.JsonAST.JValue -import akka.actor._ - -/** - * Bitcoin service over HTTP communications. - */ -trait HttpBitcoinService extends BitcoinServiceActor { - this: Actor with ActorLogging => - - /** - * Service's API - */ - protected val api: Req - - /** - * Request headers - */ - protected val headers: Map[String, String] = Map() - - /** - * Pretend to be another browser. - */ - protected val userAgent = "Windows / Chrome 34: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/537.36" - - /** - * Request and get a json response. - */ - protected def request(req: Req): JValue = ({ - Http.configure(_.setUserAgent(userAgent))(req <:< headers > as.lift.Json): Future[JValue] - })() -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/JsonRPC.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/JsonRPC.scala @@ -1,136 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import net.liftweb._ -import json.JsonAST._ - -sealed trait JsonMessage - -/** - * For more information, visit - * http://json-rpc.org/wiki/specification - */ -object JsonRPC { - implicit val formats = json.DefaultFormats - - // JSON-RPC MESSAGES - case class JsonNotification(jsonrpc: String, method: String, params: JArray) extends JsonMessage - case class JsonRequest(jsonrpc: String, id: String, method: String, params: JArray) extends JsonMessage - case class JsonResponse(jsonrpc: String, id: String, error: Option[JValue], result: Option[JValue]) extends JsonMessage { - def either: Either[String, JValue] = - (result, error) match { - case (Some(result), _) => Right(result) - case (_, Some(error)) => Left((error \ "message").extract[String]) - case _ => Left("Unknown response") - } - } - - // JsonMessage to JValue implicit - implicit def jsonRequestToJValue(r: JsonRequest): JValue = { - JObject(List( - JField("jsonrpc", JString(r.jsonrpc)), - JField("id", JString(r.id)), - JField("method", JString(r.method)), - JField("params", r.params))) - } -} - -trait WalletMessage -trait NotificationMessage extends WalletMessage - -/** - * For more information, visit - * https://en.bitcoin.it/wiki/API_reference_(JSON-RPC) - */ -object BitcoinJsonRPC { - import JsonRPC._ - - // BITCOIN TRANSACTIONS - case class RawTransaction(hex: String, txid: String, version: BigDecimal, locktime: BigDecimal, vin: Seq[VIn], vout: Seq[VOut]) - case class VIn(txid: String, vout: Int, scriptSig: ScriptSig, sequence: BigDecimal) - case class VOut(value: BigDecimal, n: BigDecimal, scriptPubKey: ScriptPubKey) - case class ScriptSig(asm: String, hex: String) - case class ScriptPubKey(asm: String, hex: String, reqSigs: BigDecimal, `type`: String, addresses: Seq[String]) - case class UnspentTransaction(txid: String, account: String, address: String, amount: BigDecimal, confirmations: BigDecimal) - case class SignedTransaction(hex: String, complete: Boolean) - case class TransactionNotification(txid: String, account: String, address: String, - category: String, amount: BigDecimal, confirmations: BigDecimal, timereceived: BigDecimal) - - // OTHER BITCOIN MESSAGES - case class AddressValidation(isvalid: Boolean, address: String, - ismine: Option[Boolean], pubkey: Option[String], iscompressed: Option[Boolean]) - - // ACTOR MESSAGES - // notifications - case class ReceivedPayment(txId: String, address: String, amount: BigDecimal, confirmations: BigDecimal) extends NotificationMessage - - // requests - sealed trait RequestMessage extends WalletMessage - case class CreateRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) extends RequestMessage - case object GetBalance extends RequestMessage - case object GetNewAddress extends RequestMessage - case class GetRawTransaction(transactionHash: String) extends RequestMessage - case class ListUnspentTransactions(minConfirmations: BigDecimal = 1, maxConfirmations: BigDecimal = 999999) extends RequestMessage - case class SendRawTransaction(signedTransaction: String) extends RequestMessage - case class SignRawTransaction(transaction: String) extends RequestMessage - case class WalletPassPhrase(walletPass: String, timeout: BigDecimal) extends RequestMessage - case class ValidateAddress(address: String) extends RequestMessage - - - /** - * Constructs JSON-RPC messages out of the existing actor messages related - * to Bitcoin's standard rpc-json commands. - */ - object JsonMessage { - def createRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) = - JsonRequest("1.0", Utils.getUUID, "createrawtransaction", JArray( - inputs.map(i => JObject(List("txid" -> i._1, "vout" -> i._2))).toList :: - receivers.map(r => JObject(List(r._1 -> r._2))).toList)) - - def getBalance = - JsonRequest("1.0", Utils.getUUID, "getbalance", Seq()) - - def getNewAddress = - JsonRequest("1.0", Utils.getUUID, "getnewaddress", Seq()) - - def getRawTransaction(transactionHash: String) = - JsonRequest("1.0", Utils.getUUID, "getrawtransaction", JArray(List(transactionHash, 1))) - - def listUnspentTransactions(minConfirmations: BigDecimal, maxConfirmations: BigDecimal, addresses: Seq[String] = Seq.empty[String]) = { - val params = - if (addresses.isEmpty) JArray(List(minConfirmations, maxConfirmations)) - else JArray(List(minConfirmations, maxConfirmations, addresses)) - JsonRequest("1.0", Utils.getUUID, "listunspent", params) - } - - def sendRawTransaction(signedTransaction: String) = - JsonRequest("1.0", Utils.getUUID, "sendrawtransaction", Seq(signedTransaction)) - - def signRawTransaction(transaction: String) = - JsonRequest("1.0", Utils.getUUID, "signrawtransaction", Seq(transaction)) - - def walletPassPhrase(walletPass: String, timeout: BigDecimal) = - JsonRequest("1.0", Utils.getUUID, "walletpassphrase", JArray(List(walletPass, timeout))) - - def validateAddress(address: String) = - JsonRequest("1.0", Utils.getUUID, "validateaddress", Seq(address)) - } - - private object Utils { - def getUUID = java.util.UUID.randomUUID.toString - } - - // JValue implicits - implicit def intToJInt(i: Int): JInt = JInt(i) - implicit def stringToJString(s: String): JString = JString(s) - implicit def stringToJField(m: (String, String)): JField = JField(m._1, m._2) - - // BigDecimal to JValue implicits - implicit def bigdecimalToJField(m: (String, BigDecimal)): JField = JField(m._1, m._2) - implicit def bigdecimalToJDouble(d: BigDecimal): JDouble = JDouble(d.doubleValue) - - // Scala collections to JArray implicits - implicit def listToJArray(l: List[JValue]): JArray = JArray(l) - implicit def seqStringToJArray(l: Seq[String]): JArray = JArray(l.map(JString).toList) -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/WsBitcoinService.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/WsBitcoinService.scala @@ -1,200 +0,0 @@ -package inc.pyc.chimera -package bitcoin -package service - -import java.net.{URI, ConnectException} -import concurrent._ -import duration._ -import collection._ -import collection.JavaConversions._ -import BitcoinServices.system.dispatcher -import scala.util.{Success, Failure} -import akka.actor._ -import akka.pattern._ -import akka.util._ -import net.liftweb.json._ -import JsonAST.JValue -import org.java_websocket._ -import client._ -import handshake._ -import drafts._ - - -/** - * Bitcoin service over WebSocket communications. - */ -trait WsBitcoinService extends BitcoinServiceActor { - this: Actor with ActorLogging => - - protected implicit val timeout: Timeout - - /** - * Service's API - */ - protected val api: URI - - /** - * Handle json messages sent from server. - */ - protected def onMessage(msg: JValue): () => Unit - - /** - * Request headers - */ - protected val headers: Map[String,String] = Map() - - /** - * Maps request IDs to the corresponding response promises - * and a function that converts the JSON RPC response to the final actor response. - */ - protected val requests = mutable.HashMap.empty[String, (Promise[Any], JValue => _, List[String])] - - /** - * Executes on websocket initial connection. - */ - protected def onConnect: () => Unit = () => {} - - /** - * Request and forget. - */ - protected def requestForget(req: JValue): Unit = { - request(req) - } - - /** - * Request and extract promised json response. - * Note: Must be an actor making the request to receive response. - */ - protected def requestExtract[T](id: String, req: JValue, extractor: JValue => T, info: List[String] = Nil): Unit = { - - val p = Promise[Any]() - val f = p.future - requests += id -> (p, extractor, info) - request(req) - - context.system.scheduler.scheduleOnce(timeout.duration) { - p tryFailure { - new TimeoutException(s"Timeout: $serviceName bitcoin wallet did not respond in time") - } - self ! RemoveWsRequest(id) - } - - pipe(f) to sender - } - - override def preStart(): Unit = { - context.become(connecting) - tryToConnect() - } - - /** - * Receive when disconnected. - */ - def connecting: Receive = { - case Connected => - context.become(receive) - onConnect() - - case _ => - val message = s"Cannot process request: no connection to $serviceName websocket." - sender ! Status.Failure(new IllegalStateException(message)) - log.error(message) - } - - /** - * Receive when connected. - */ - val websocket: Receive = { - case RemoveWsRequest(id) => - requests -= id - - case Disconnected(reason) => - log.warning("Connection to {} bitcoin wallet closed: {}", serviceName, reason) - context.become(connecting) - tryToConnect() - } - - /** - * Tries to connect to the websocket. - * @param f function to manipulate the websocket object before trying to connect. - */ - protected def tryToConnect(f: WebSocketBtcWalletClient => WebSocketBtcWalletClient = walletClient => walletClient) { - requests.clear() - walletClient = new WebSocketBtcWalletClient(api, new Draft_17, headers, 0) - walletClient = f(walletClient) - val connected = walletClient.connectBlocking() - - if (connected) { - log.info("Connection to {} bitcoin wallet established", serviceName) - self ! Connected - } else { - new ConnectException(s"$serviceName bitcoin wallet not available: $api") - } - } - - /* Send request down websocket. */ - private def request(req: JValue) { walletClient.send(compact(render(req))) } - - /* Personal websocket class of the java-websocket library. */ - class WebSocketBtcWalletClient(serverUri: URI, protocolDraft: Draft, httpHeaders: Map[String, String], connectTimeout: Int) - extends WebSocketClient(serverUri, protocolDraft, headers, connectTimeout) { - override def onMessage(jsonMessage: String): Unit = { ws.onMessage(parse(jsonMessage)) } - override def onOpen(handshakeData: ServerHandshake) { } - override def onClose(code: Int, reason: String, remote: Boolean) { self ! Disconnected(reason) } - override def onError(ex: Exception) { self ! Disconnected(ex.getMessage()) } - } - - /* The actual websocket. */ - protected var walletClient: WebSocketBtcWalletClient = null - - /* Used by the websocket class to access actor methods with same name. */ - private def ws = this - - /* Websocket Messages */ - case object Connected - case class Disconnected(reason: String) - case class RemoveWsRequest(id: String) -} - -/** - * Bitcoin service over WebSocket communications with SSL. - */ -trait WssBitcoinService extends WsBitcoinService { - this: Actor with ActorLogging => - - import java.io._ - import java.security._ - import javax.net.ssl._ - - /** - * KeyStore file containing trusted certificates. - */ - val keyStoreFile: File - - /** - * Password of the KeyStore `keyStoreFile` - */ - val keyStorePass: String - - private val socketFactory = createSslSocketFactory - - override def tryToConnect(f: WebSocketBtcWalletClient => WebSocketBtcWalletClient = walletClient => walletClient) = - super.tryToConnect( walletClient => { - walletClient.setSocket(socketFactory.createSocket()) - walletClient - }) - - /** - * Creates a ssl socket factory for the websocket using the - * given keystore file and password. - */ - private def createSslSocketFactory: SSLSocketFactory = { - val ks = KeyStore.getInstance("JKS") - ks.load(new FileInputStream(keyStoreFile), keyStorePass.toCharArray) - val tmf = TrustManagerFactory.getInstance("SunX509") - tmf.init(ks) - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, tmf.getTrustManagers, null) - sslContext.getSocketFactory - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/comet/PYCActor.scala b/src/main/scala/inc/pyc/chimera/comet/PYCActor.scala @@ -1,32 +0,0 @@ -package inc.pyc.chimera -package comet - -import net.liftweb._ -import actor._ -import http._ - -object PYCSettings extends Factory { - - /** Interest Rate per bitcoin. */ - val interestRate = new FactoryMaker[Float](0) {} - - -} - -/** - * Bridge to access PYC central server for ATMs. - */ -object PYCActor extends LiftActor with ListenerManager { - import PYCSettings._ - - protected def createUpdate = "" - - override def lowPriority = { - case InterestRate(rate) => - interestRate.default.set(rate) - updateListeners(rate) - } -} - -/** Interest Rate per bitcoin. */ -case class InterestRate(rate: Float) -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/config/Machine.scala b/src/main/scala/inc/pyc/chimera/config/Machine.scala @@ -1,17 +1,70 @@ package inc.pyc.chimera package config -import lycia._ +import model._ +import lib._ +import bill.acceptor._ +import driver._ +import currency._ +import akka.actor._ import akka.agent._ -import akka.actor.ActorSystem -import concurrent._ -import net.liftweb.util.Props +import com.typesafe.config._ +import inc.pyc.bitcoin._ +import inc.pyc.lift_akka.LiftActorEventBus object Machine { - - implicit val system = ActorSystem("MachineConfig") import system.dispatcher - val id = Agent(Props.get("chimera.id", "default")) - val name = Agent(Lycia.machineName) + private val config: Config = ConfigFactory.load().getConfig("chimera") + + val id: String = config.getString("id") + val name: String = config.getString("name") + val currency = findCurrency(config.getString("currency")) + val billAcceptorDriver = findBillAcceptorDriver(config.getString("bill-acceptor")) + + /** + * Hardware Actor System + */ + val system = ActorSystem("ChimeraSystem") + + + /** + * Event Bus for Chimera internal. + */ + val bus = new LookupSystem + + /** + * Subscription topics for event bus. + */ + object topics { + val redirectTo = "redirectTo" + val transactionUpdate = "transactionUpdate" + } + + /** + * The main bitcoin wallet + */ + val wallet = Wallet(system) + + /** + * The main bitcoin price ticker + */ + val priceTicker = PriceTicker(system) + + /** + * The transaction of the current user. + */ + val currentTransaction: Agent[Transaction] = Agent(Transaction.create) + + /** + * The machine's bill acceptor + */ + val billAcceptor = BillAcceptor(system) +} + +/** + * PubSub for hardware related topics. + */ +class LookupSystem extends LiftActorEventBus { + override protected def mapSize: Int = 50 } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/Network.scala b/src/main/scala/inc/pyc/chimera/lib/Network.scala @@ -0,0 +1,80 @@ +package inc.pyc.chimera +package lib + +import config._ +import inc.pyc.lift_akka.EventUpdate +import akka.actor._ +import scala.concurrent.duration._ +import dispatch._ + +/** + * Checking network connectivity every n seconds. + */ + +object Network extends ExtensionId[NetworkImpl] with ExtensionIdProvider { + override def lookup = Network + override def createExtension(system: ExtendedActorSystem) = new NetworkImpl(system) +} + +class NetworkImpl(system: ExtendedActorSystem) extends Extension { + import system.dispatcher + import commands._ + + /** + * The main actor that handles communications. + */ + private val actor = system.actorOf(Props[Network], "PingActor") + + /** + * The ticker that checks network connection every 20 seconds. + */ + private val ticker = + system.scheduler.schedule(0 milliseconds, 20 seconds, actor, commands.Ping) + + def ping(): Unit = actor ! Ping +} + +class Network extends Actor with ActorLogging { + import commands._ + import Defaults._ + + val ping = "https://www.google.com/" + + def receive: Receive = { + case Ping => + check(ping, fail = Some("/disconnected")) + + case "init" => + log.info("Network Connectivity Actor Activated") + } + + def disconnected: Receive = { + case Ping => + check(ping, succeed = Some("/index")) + } + + def check(link: String, fail: Option[String] = None, succeed: Option[String] = None) { + Http(url(link) OK as.String).either() match { + case Left(_) => + fail map { + redirect => + log.info("Network unreachable: redirecting to {}", redirect) + context.become(disconnected) + Machine.bus.publish(EventUpdate(Machine.topics.redirectTo, redirect)) + } + + case Right(_) => + succeed map { + redirect => + log.info("Network reconnected: redirecting to {}", redirect) + context.become(receive) + Machine.bus.publish(EventUpdate(Machine.topics.redirectTo, redirect)) + } + } + } +} + + +private object commands { + case object Ping + } +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/BillAcceptor.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/BillAcceptor.scala @@ -0,0 +1,112 @@ +package inc.pyc.chimera +package lib +package bill.acceptor + +import config._ +import currency._ +import driver._ +import akka.actor._ +import akka.pattern.ask +import akka.util.Timeout +import scala.concurrent.duration._ +import inc.pyc.lift_akka.EventUpdate + + +object BillAcceptor extends ExtensionId[BillAcceptorImpl] with ExtensionIdProvider { + override def lookup = BillAcceptor + override def createExtension(system: ExtendedActorSystem) = new BillAcceptorImpl(system) + + /** + * Subscription topics for event bus. + */ + val insertedBill = "insertedBill" +} + + +/** + * Bill Acceptor API implementation + */ +class BillAcceptorImpl(system: ExtendedActorSystem) extends Extension { + import commands._ + + /** + * The main actor that handles communications. + */ + private val actor = system.actorOf(Props[BillAcceptor], "BillAcceptor") + + def listen(): Unit = actor ! Listen + + def unlisten(): Unit = actor ! UnListen +} + + +class BillAcceptor extends Actor with ActorLogging { + import commands._ + import Machine.bus + import Machine.topics._ + import context.dispatcher + + implicit val timeout = Timeout(2 seconds) + + /** The currency that is currently accepted. */ + private var currency: Currency = Machine.currency + + /** Bill Acceptor Driver */ + private val driver = initDriver(Machine.billAcceptorDriver) + + def receive = { + + case Inserted(bill) => + log.info(s"Inserted bill: $bill $currency") + bus.publish(EventUpdate(BillAcceptor.insertedBill, bill)) + + case Listen => + (driver ? Listen).mapTo[Boolean] map { success => + if(!success) { + log.error("Failed to listen for bills") + bus.publish(EventUpdate(redirectTo, "/malfunction")) + } else { + log.info("Listening for bills") + } + } + + case UnListen => + (driver ? UnListen).mapTo[Boolean] map { success => + if(!success) { + log.error("Failed to stop listening for bills") + bus.publish(EventUpdate(redirectTo, "/malfunction")) + } else { + log.info("Stopped listening for bills") + } + } + + case _ => + log.warning("Received Unknown Message") + } + + private def initDriver(service: BillAcceptorDriver.Value): ActorRef = { + context.actorOf(BillAcceptorDriver.getDriverActor(service), service.toString) + } +} + + +private[acceptor] object commands { + + /** + * Command to notify that a bill was inserted + * and the value of the bill is `bill` + */ + case class Inserted(bill: Int) + + /** + * Command to turn on the driver and + * listen for inserted bills. + */ + case object Listen + + /** + * Command to turn off the driver and + * stop listening for inserted bills. + */ + case object UnListen +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/APEX.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/APEX.scala @@ -0,0 +1,12 @@ +package inc.pyc.chimera.lib +package bill.acceptor +package driver + +import akka.actor._ + +class APEX extends Actor { + + def receive = { + case _ => + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/Driver.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/Driver.scala @@ -0,0 +1,23 @@ +package inc.pyc.chimera.lib +package bill.acceptor +package driver + +import akka.actor._ + + +/** + * The different choices for bitcoin services. + */ +object BillAcceptorDriver extends Enumeration { + type BillAcceptorDriver = Value + val JCM, APEX = Value + + def drivers: Map[BillAcceptorDriver, Props] = Map( + JCM -> Props[JCM], + APEX -> Props[APEX] + ) + + def getDriverActor(s: Value): Props = { + drivers.filter(_._1 == s).map(_._2).head + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/JCM.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/JCM.scala @@ -0,0 +1,12 @@ +package inc.pyc.chimera.lib +package bill.acceptor +package driver + +import akka.actor._ + +class JCM extends Actor { + + def receive = { + case _ => + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/package.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/package.scala @@ -0,0 +1,11 @@ +package inc.pyc.chimera.lib.bill.acceptor + +package object driver { + + import driver._ + import BillAcceptorDriver._ + + def findBillAcceptorDriver(s: String): BillAcceptorDriver = { + drivers.filter(_._1.toString == s).map(_._1).head + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/currency/Currency.scala b/src/main/scala/inc/pyc/chimera/lib/currency/Currency.scala @@ -0,0 +1,12 @@ +package inc.pyc.chimera.lib +package currency + +abstract class Currency extends Enumeration { + + /** + * Symbol of the currency + */ + val symbol: String + + implicit def toInt(c: Value): Int = c.toString.toInt +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/currency/USD.scala b/src/main/scala/inc/pyc/chimera/lib/currency/USD.scala @@ -0,0 +1,8 @@ +package inc.pyc.chimera.lib +package currency + +object USD extends Currency { + type USD = Value + val symbol = "$" + val `1`, `5`, `10`, `20`, `50`, `100` = Value +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/currency/package.scala b/src/main/scala/inc/pyc/chimera/lib/currency/package.scala @@ -0,0 +1,11 @@ +package inc.pyc.chimera.lib + +package object currency { + + val currencies: Map[String, Currency] = Map( + "USD" -> USD + ) + + def findCurrency(s: String): Currency = + currencies.filter(_._1 == s).map(_._2).head +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lycia/Lycia.scala b/src/main/scala/inc/pyc/chimera/lycia/Lycia.scala @@ -1,34 +1,12 @@ package inc.pyc.chimera package lycia -import config._ -import bitcoin.service._ -import akka.actor._ -import akka.event._ -import inc.pyc.chimera.bitcoin.BitcoinService +import inc.pyc.bitcoin._ object Lycia { - def !(x: Any): Unit = actor ! x - - /** - * Lycia Actor System - */ - val system = ActorSystem("LyciaSystem") - - /** - * The main actor that handles communications. - */ - private val actor = system.actorOf(Props[Lycia], "Lycia") - - def machineName: String = { - // get name of machine with given name - Machine.id() - "" - } - def buyLimit: Double = { - 500 + 1000 } def percentProfit: Double = { @@ -44,10 +22,4 @@ object Lycia { // TODO get service configured BitcoinService.BlockChain } -} - -class Lycia extends Actor { - def receive = { - case _ => - } } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/model/Transaction.scala b/src/main/scala/inc/pyc/chimera/model/Transaction.scala @@ -1,8 +1,8 @@ package inc.pyc.chimera package model +import lib.currency.Currency import net.liftweb._ -import util._ import mapper._ import json._ import JsonDSL._ @@ -30,17 +30,34 @@ class Transaction extends KeyedMapper[String, Transaction] { object price extends MappedString(this, 256) /** - * Buy limit (in USD) + * Buy limit */ object limit extends MappedDouble(this) /** + * Currency used to make the transaction + */ + object currency extends MappedString(this, 5) { + def apply(c: Currency) = { + currencySymbol(c) + super.apply(c.toString) + } + } + + /** + * Symbol used by the currency. + */ + object currencySymbol extends MappedString(this, 5) { + def apply(c: Currency) = super.apply(c.symbol) + } + + /** * Whether we ever scanned the address before */ object newAddress extends MappedBoolean(this) /** - * The USD bills the user has inserted into the bill acceptor + * The bills the user has inserted into the bill acceptor */ object billsInserted extends MappedString(this, 2048) { def asList: List[Int] = @@ -95,6 +112,8 @@ class Transaction extends KeyedMapper[String, Transaction] { (address.name -> address.get) ~ (price.name -> price.get) ~ (limit.name -> limit.get) ~ + (currency.name -> currency.get) ~ + (currencySymbol.name -> currencySymbol.get) ~ (newAddress.name -> newAddress.get) ~ (billsInserted.name -> billsInserted.asList) ~ (sent.name -> sent.get) ~ diff --git a/src/main/scala/inc/pyc/chimera/snippet/BitcoinAddressScanner.scala b/src/main/scala/inc/pyc/chimera/snippet/BitcoinAddressScanner.scala @@ -1,28 +1,28 @@ package inc.pyc.chimera package snippet +import config._ +import Machine._ +import system.dispatcher import model._ -import bitcoin._ import lycia._ -import TransactionState._ -import BitcoinServices.system.dispatcher import net.liftweb._ import http._ import json.JsonAST._ import json.JsonDSL._ -import net.liftweb.common.Logger import scala.concurrent.Future +import inc.pyc.lift_akka._ -class BitcoinAddressScanner extends RoundTripSnippet with Logger { +class BitcoinAddressScanner extends RoundTripSnippet { def roundTrips: List[RoundTripInfo] = List("submitAddress" -> submitAddress _) def submitAddress(json: JValue): Message = for (JString(address) <- json) yield for { - validation <- Wallet.validateAddress(address) - price <- PriceTicker.price() + validation <- wallet.validateAddress(address) + price <- priceTicker.price() } yield if (!validation.isvalid) { Message("failure") @@ -32,6 +32,7 @@ class BitcoinAddressScanner extends RoundTripSnippet with Logger { Transaction.create .address(address) .price(price.format) + .currency(Machine.currency) .limit(Lycia.buyLimit) .currentPage("Insert Bills") @@ -47,11 +48,18 @@ class BitcoinAddressScanner extends RoundTripSnippet with Logger { val ctx = currentTransaction() if(ctx.billsInserted.asList.isEmpty) { currentTransaction send tx - txBus.publish(EventUpdate(topics.transactionUpdate, tx.asJValue)) + bus.publish(EventUpdate(topics.transactionUpdate, tx.asJValue)) } } } Message("success", data = Some(tx.asJValue)) } + /* + def listenBillAcceptor(json: JValue): Message = { + info("justa testin") + Machine.billAcceptor.listen() + Message("success") + } + */ } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/ClientActorBridge.scala b/src/main/scala/inc/pyc/chimera/snippet/ClientActorBridge.scala @@ -1,106 +0,0 @@ -package inc.pyc.chimera -package snippet - -import scala.xml._ -import net.liftweb._ -import common._ -import actor._ -import http._ -import js._ -import JE._ -import JsCmds._ -import json._, Extraction.decompose -import net.liftmodules.extras.JsExtras -import akka.event.{EventBus, LookupClassification} - -/** - * Lift snippet to initiate a comet actor that subscribes to a - * topic in an Akka event bus system. Client-side javascript Actor - * Bridge must have a function with the topic name to receive updates. - */ -abstract class EventRegister extends ClientEventPushBridge with ImplicitSnip { - - val topics: List[(String, () => Unit)] - - def render(in: NodeSeq): NodeSeq = - topics map (x => connectActorBridge(x._1, x._2, in)) -} - -/** - * Bridge to push messages from akka's event bus system to Actor Bridge in javascript. - * - * The topic being listened client-side should be the same as the - * topic in the event bus server-side. - */ -trait ClientEventPushBridge extends ClientActorBridge { - - val bus: LiftActorEventBus - - protected def connectActorBridge(topic: String, postSubscribe: () => Unit, in: NodeSeq): NodeSeq = - super.connectActorBridge(topic, in, { - (session, clientProxy) => - - val serverActor = new ScopedLiftActor with LiftActorCompare { - override def lowPriority = { - case payload => - clientProxy ! payload - } - } - - bus.subscribe(serverActor, topic) - postSubscribe() - serverActor - }) -} - -/** - * The bridge between ActorBridge in javascript and server-side actors. - */ -trait ClientActorBridge extends Logger { - - /** - * Connects a given `ScopedLiftActor` to the given function name listening from - * the client-side Actor Bridge. - */ - protected def connectActorBridge(topic: String, in: NodeSeq, f: (LiftSession, LiftActor) => ScopedLiftActor): NodeSeq = - (for (sess <- S.session) yield { - val clientProxy = sess.serverActorForClient("window.actorsBridge."+topic) - - val serverActor: ScopedLiftActor = f(sess, clientProxy) - //val className: String = this.getClass.getName.split("""\.""").last - - S.appendJs(SetExp(JsVar("window.actorsBridge"), - JsExtras.CallNew("window.ActorsBridge", sess.clientActorFor(serverActor)) )) - - in - }) openOr NodeSeq.Empty - -} - - -/** - * Message to update `LiftActorEventBus`. - */ -case class EventUpdate(topic: String, payload: Any) - -/** - * Bridge between Akka's event bus and Lift actors. - */ -abstract class LiftActorEventBus extends EventBus with LookupClassification { - type Event = EventUpdate - type Classifier = String - type Subscriber = LiftActorCompare - - override protected def classify(event: Event): Classifier = event.topic - - override protected def publish(event: Event, subscriber: Subscriber): Unit = { - subscriber ! event.payload - } - - override protected def compareSubscribers(a: Subscriber, b: Subscriber): Int = - a.compareTo(b) -} - -sealed trait LiftActorCompare extends LiftActor with java.lang.Comparable[LiftActor] { - override def compareTo(a: LiftActor): Int = -1 -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/Comet.scala b/src/main/scala/inc/pyc/chimera/snippet/Comet.scala @@ -0,0 +1,84 @@ +package inc.pyc.chimera +package snippet + +import config._ +import lib._ +import bill.acceptor._ +import xml._ +import Machine.topics._ +import BillAcceptor.insertedBill +import inc.pyc.lift_akka.EventRegister +import inc.pyc.bitcoin._ +import net.liftweb.json._ +import JsonDSL._ + + +/** + * Snippet to update the client-side transaction. + */ +class Transaction extends EventRegister { + + val system = Machine.system + val bus = Machine.bus + val topic = transactionUpdate + + override val receive: Receive = { + case JField("msg", JString("buyBitcoin")) => + // TODO buy the bitcoin + } + + def render(in: NodeSeq) = comet(in) +} + + +/** + * Snippet to update screen when bills are inserted. + */ +class BillAcceptor extends EventRegister { + + val system = Machine.system + val bus = Machine.bus + val topic = insertedBill + + override val receive: Receive = { + case JString("listen") => + Machine.billAcceptor.listen() + + case JString("unlisten") => + Machine.billAcceptor.unlisten() + } + + def render(in: NodeSeq) = comet(in) +} + + +/** + * Snippet to update the price ticker on screen. + */ +class PriceTicker extends EventRegister { + + val system = Machine.system + val bus = BitcoinSystem.bus + val topic = PriceTicker.newPrice + + override val postSubscribe = + /*TraceRecorder.withNewTraceContext("priceticker")*/ () => { + Machine.priceTicker.gossipPrice + } + + def render(in: NodeSeq) = comet(in) +} + + +/** + * Snippet to redirect the client when needed. + */ +class Redirect extends EventRegister { + + val system = Machine.system + val bus = Machine.bus + val topic = redirectTo + + + def render(in: NodeSeq) = comet(in) +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/Network.scala b/src/main/scala/inc/pyc/chimera/snippet/Network.scala @@ -1,119 +0,0 @@ -package inc.pyc.chimera -package snippet - -import xml._ -import model._ -import akka.actor._ -import akka.agent._ -import dispatch._, Defaults._ -import scala.concurrent.duration._ -import org.joda.time.DateTime - -/** - * Checking network connectivity every n seconds. - */ -object Network { - - def !(any: Any): Unit = actor ! any - - /** - * Transaction State Actor System - */ - val system = ActorSystem("NetworkSystem") - - - /** - * Event Bus for Transaction State. - */ - val bus = new LookupTransaction - - /** - * The main actor that handles communications. - */ - private val actor = system.actorOf(Props[Network], "PingActor") - - /** - * The ticker that checks network connection every 20 seconds. - */ - private val ticker = - system.scheduler.schedule(0 milliseconds, 20 seconds, actor, commands.Ping) - - /** - * Subscription topics for event bus. - */ - object topics { - val redirectTo = "redirectTo" - } - - object commands { - case object Ping - } -} - - -/** - * PubSub for network. - */ -class LookupNetwork extends LiftActorEventBus { - // redirectTo - override protected def mapSize: Int = 1 -} - -class Network extends Actor with ActorLogging { - import Network.commands._ - import Network.topics._ - - val ping = "https://www.google.com/" - - def receive: Receive = { - case Ping => - check(ping, fail = Some("/disconnected")) - - case "init" => - log.info("Network Connectivity Actor Activated") - } - - def disconnected: Receive = { - case Ping => - check(ping, succeed = Some("/index")) - } - - def check(link: String, fail: Option[String] = None, succeed: Option[String] = None) { - Http(url(link) OK as.String).either() match { - case Left(_) => - fail map { - redirect => - log.info("Network unreachable: redirecting to {}", redirect) - context.become(disconnected) - Network.bus.publish(EventUpdate(redirectTo, redirect)) - } - - case Right(_) => - succeed map { - redirect => - log.info("Network reconnected: redirecting to {}", redirect) - context.become(receive) - Network.bus.publish(EventUpdate(redirectTo, redirect)) - } - } - } -} - -/** - * Snippet for updates on network connectivity. - */ -class NetworkComet extends EventRegister { - - implicit val system = Network.system - - val bus = Network.bus - - val topics: List[(String, () => Unit)] = List( - (Network.topics.redirectTo, () => {})) - - - - override def render(o: NodeSeq): NodeSeq = { - super.render(o) - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/PriceTicker.scala b/src/main/scala/inc/pyc/chimera/snippet/PriceTicker.scala @@ -1,32 +0,0 @@ -package inc.pyc.chimera -package snippet - -import lycia._ -import bitcoin._ -import PriceTicker.commands._ -import xml._ -import akka.actor.ActorSystem - - -/** - * Snippet to update the price ticker on screen. - */ -class PriceTickerComet extends EventRegister { - - implicit val system = ActorSystem("PriceTickerComet") - - val bus = BitcoinServices.bus - - val topics: List[(String, () => Unit)] = List( - (PriceTicker.topics.newPrice, initPriceTicker)) - - def initPriceTicker() = - /*TraceRecorder.withNewTraceContext("priceticker")*/ { - PriceTicker ! Percentage(Lycia.percentProfit) - PriceTicker ! GossipPrice - } - - override def render(o: NodeSeq): NodeSeq = { - super.render(o) - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/Transaction.scala b/src/main/scala/inc/pyc/chimera/snippet/Transaction.scala @@ -1,61 +0,0 @@ -package inc.pyc.chimera -package snippet - -import xml._ -import model._ -import akka.actor._ -import akka.agent._ - -/** - * Handling state of the current transaction. - */ -object TransactionState { - - /** - * Transaction State Actor System - */ - val system = ActorSystem("TransactionSystem") - - /** - * Event Bus for Transaction State. - */ - val txBus = new LookupTransaction - - /** - * Subscription topics for event bus. - */ - object topics { - val transactionUpdate = "transactionUpdate" - } - - import system.dispatcher - - val currentTransaction: Agent[Transaction] = Agent(Transaction.create) -} - - -/** - * PubSub for transaction updates. - */ -class LookupTransaction extends LiftActorEventBus { - // transactionUpdate - override protected def mapSize: Int = 1 -} - -/** - * Snippet to update the current transaction. - */ -class TransactionComet extends EventRegister { - - val system = TransactionState.system - - val bus = TransactionState.txBus - - val topics: List[(String, () => Unit)] = List( - (TransactionState.topics.transactionUpdate, () => {})) - - - override def render(o: NodeSeq): NodeSeq = { - super.render(o) - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/UtilSnips.scala b/src/main/scala/inc/pyc/chimera/snippet/UtilSnips.scala @@ -1,6 +1,7 @@ package inc.pyc.chimera package snippet +import config.Machine._ import scala.xml.NodeSeq import net.liftweb._ import util._ @@ -16,13 +17,6 @@ object ProductionOnly { else NodeSeq.Empty } -trait MsgData - - -trait ImplicitSnip { - implicit def seqNodeSeqToNodeSeq(in: Seq[NodeSeq]): NodeSeq = in.headOption.getOrElse(NodeSeq.Empty) -} - /* * This is used to send messages to client. */ diff --git a/src/main/scala/inc/pyc/lift_akka/ClientActorBridge.scala b/src/main/scala/inc/pyc/lift_akka/ClientActorBridge.scala @@ -0,0 +1,127 @@ +package inc.pyc.lift_akka + +import scala.xml._ +import net.liftweb._ +import net.liftweb.common._ +import net.liftweb.actor._ +import net.liftweb.http._ +import net.liftweb.http.js._ +import net.liftweb.http.js.JE._ +import net.liftweb.http.js.JsCmds._ +import net.liftweb.json._ +import net.liftmodules.extras.JsExtras +import akka.event.{EventBus, LookupClassification} + +/** + * Lift snippet to initiate a comet actor that subscribes to a + * topic in an Akka event bus system. Client-side javascript Actor + * Bridge must have a function with the topic name to receive updates. + */ +abstract class EventRegister extends ClientEventPushBridge { + def comet(in: NodeSeq): NodeSeq = connectActorBridge(in) +} + +/** + * Bridge to push messages from akka's event bus system to Actor Bridge in javascript. + * + * The topic being listened client-side should be the same as the + * topic in the event bus server-side. + */ +trait ClientEventPushBridge extends ClientActorBridge { + + type Receive = PartialFunction[Any, Unit] + + + /** + * The event bus + */ + val bus: LiftActorEventBus + + /** + * Callback function after topic subscription of the event bus. + */ + val postSubscribe: () => Unit = () => {} + + /** + * The partial function to handle requests + * coming from the server or client. + */ + val receive: Receive = { + case JString(str) => info("he he he he: "+str) + } + + protected def connectActorBridge(in: NodeSeq): NodeSeq = + super.connectActorBridge(topic, in, { + (session, clientProxy) => + + val serverActor = new ScopedLiftActor with LiftActorCompare { + override def lowPriority = receive orElse { + case event => + clientProxy ! event + } + } + + bus.subscribe(serverActor, topic) + postSubscribe() + serverActor + }) +} + +/** + * The bridge between ActorBridge in javascript and server-side actors. + */ +trait ClientActorBridge extends Logger { + + /** + * Topic (or function) to execute client-side. + */ + val topic: String + + /** + * Connects a given `ScopedLiftActor` to the given function name listening from + * the client-side Actor Bridge. + */ + protected def connectActorBridge(topic: String, in: NodeSeq, f: (LiftSession, LiftActor) => ScopedLiftActor): NodeSeq = + (for (sess <- S.session) yield { + val className = this.getClass.getName.split("""\.""").last + val instaName = lowercase(className) + + val clientProxy = sess.serverActorForClient(s"window.$instaName.apply") + val serverActor: ScopedLiftActor = f(sess, clientProxy) + + S.appendJs(SetExp(JsVar(s"window.$instaName"), + JsExtras.CallNew(s"window.$className", sess.clientActorFor(serverActor)) )) + + in + }) openOr NodeSeq.Empty + + private def lowercase(s: String): String = s(0).toLower + s.substring(1) +} + + +/** + * Message to update `LiftActorEventBus`. + */ +case class EventUpdate(topic: String, payload: Any) + +/** + * Bridge between Akka's event bus and Lift actors. + */ +abstract class LiftActorEventBus extends EventBus with LookupClassification { + type Event = EventUpdate + type Classifier = String + type Subscriber = LiftActorCompare + + override protected def classify(event: Event): Classifier = event.topic + + override protected def publish(event: Event, subscriber: Subscriber): Unit = { + subscriber ! event.payload + } + + override protected def compareSubscribers(a: Subscriber, b: Subscriber): Int = + a.compareTo(b) +} + +sealed trait LiftActorCompare extends LiftActor with java.lang.Comparable[LiftActor] { + override def compareTo(a: LiftActor): Int = -1 +} +\ No newline at end of file diff --git a/src/main/webapp/app/ActorsBridge.js b/src/main/webapp/app/ActorsBridge.js @@ -1,44 +1,78 @@ + +/** + * Broadcasts message to angular app in html document. + */ +window.broadcast = function(event, message) { + var scope = angular.element('[ng-app]').scope(); + scope.$apply(function () { + scope.$broadcast(event, message); + }); +}; + +/** + * Price Ticker Comet + * Updates the price for price ticker on screen. + */ +window.PriceTicker = function(sendFunc) { + "use strict"; + var self = this; + + self.send = sendFunc; + + self.apply = function(message) { + window.broadcast('newPrice', message); + }; +}; + +/** + * Transaction Comet + * Updates the state of the current transaction. + */ +window.Transaction = function(sendFunc) { + "use strict"; + var self = this; + + self.send = sendFunc; + + self.apply = function(message) { + window.broadcast('transactionUpdate', message); + }; + + /** + * Updates the state of the bills inserted. + */ + self.insertedBill = function(message) { + window.broadcast('insertedBill', message); + }; +}; + +/** + * Bill Acceptor Comet + * Updates the state of the bills inserted. + */ +window.BillAcceptor = function(sendFunc) { + "use strict"; + var self = this; + + self.send = sendFunc; + + self.apply = function(message) { + window.broadcast('insertedBill', message); + }; +}; + /** - * Actors Bridge: comet updates are handled here. + * Redirect Comet + * Used for page redirection. For example, when the network + * is down, the page will redirect to the appropriate page. */ -window.ActorsBridge = function(sendFunc) { - "use strict"; - - var self = this; - - self.send = sendFunc; - - /** - * Broadcasts message to angular app in html document. - */ - self.broadcast = function(event, message) { - var scope = angular.element('[ng-app]').scope(); - scope.$apply(function () { - scope.$broadcast(event, message); - }); - }; - - /** - * Updates the price for price ticker. - */ - self.newPrice = function(message) { - self.broadcast('newPrice', message); - }; - - /** - * Updates the state of the current transaction. - */ - self.transactionUpdate = function(message) { - self.broadcast('transactionUpdate', message); - }; - - /** - * Used for page redirection. For example, when the network - * is down, the page will redirect to the appropriate page. - */ - self.redirectTo = function(message) { - window.console.log(message); - window.location = message; - }; - +window.Redirect = function(sendFunc) { + "use strict"; + var self = this; + + self.send = sendFunc; + + self.apply = function(message) { + window.location = message; + }; }; \ No newline at end of file diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -41,6 +41,26 @@ app.controller('MainCtrl', ['$scope', '$rootScope', function($scope, $rootScope) $rootScope.$on("transactionUpdate", function (event, message) { jQuery.extend($rootScope.transaction, message); }); + + $rootScope.$on("insertedBill", function (event, message) { + $rootScope.transaction.billsInserted.push(message); + }); + + /** + * Total amount inserted in the bill acceptor. + */ + $scope.totalAmount = function() { + var bills = $rootScope.transaction.billsInserted; + return bills.reduce(function(a, b) { return a + b; }); + }; + + /** + * Bitcoin amount compared to the total amount inserted in the bill acceptor. + */ + $scope.bitcoinAmount = function() { + return $scope.totalAmount / $rootScope.transaction.price; + }; + }]); @@ -78,6 +98,7 @@ app.controller('WalletScannerCtrl', ['$scope', '$rootScope', '$controller', '$ti } WizardHandler.wizard().next(); + window.billAcceptor.send("listen"); }; var failure = function() { @@ -97,26 +118,4 @@ app.controller('WalletScannerCtrl', ['$scope', '$rootScope', '$controller', '$ti window.console.log("QR Video Error: "+error); // TODO log it in redis }; -}]); - -/** - * User registration form controller. - */ -app.controller('UserRegistrationCtrl', ['$scope', '$controller', '$rootScope', function($scope, $controller, $rootScope) { - $controller('FormCtrl', {$scope: $scope}); - - $scope.save = function() { - $scope.loading = true; - window.UserRegistration.submit($scope.model).then(function(alert) { - $scope.$apply(function() { - $scope.loading = false; - if(alert.msg_type === "success") { - $rootScope.$broadcast('alertDialog', alert); - $scope.reset(); - } else { - $rootScope.$broadcast('alertDialog', alert); - } - }); - }); - }; }]); \ No newline at end of file diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html @@ -24,14 +24,18 @@ <div>{{ transaction.address }}</div> <div>{{ transaction.price }}</div> <div>{{ transaction.limit }}</div> + <div>Total: {{ transaction.currencySymbol + totalAmount }}</div> + <div>bitcoin: <i class="fa fa-btc"></i>{{ bitcoinAmount }}</div> + <input type="submit" wz-next value="Finish now" class="btn btn-primary" /> </wz-step> </wizard> <!-- Comets --> <div data-lift="tail"> - <script data-lift="PriceTickerComet"></script> - <script data-lift="TransactionComet"></script> + <script data-lift="PriceTicker"></script> + <script data-lift="Transaction"></script> + <script data-lift="BillAcceptor"></script> </div> </div> \ No newline at end of file diff --git a/src/main/webapp/malfunction.html b/src/main/webapp/malfunction.html @@ -0,0 +1,3 @@ +<div data-lift="surround?with=base-default;at=content"> + <p style="font-size: 1.2em;">We're sorry but there's a hardware malfunction.</p> +</div> +\ No newline at end of file diff --git a/src/main/webapp/templates-hidden/base-wrap.html b/src/main/webapp/templates-hidden/base-wrap.html @@ -36,7 +36,7 @@ <script data-lift="Assets.js" type="text/javascript"></script> <!-- Comets --> -<script data-lift="NetworkComet"></script> +<script data-lift="Redirect"></script> </body> </html>