bitcoin-atm
bitcoin atm for pyc inc.
git clone https://9o.is/git/bitcoin-atm.git
commit 56184dc361c32a561d0b75926f129690c001f42f parent 5c87c6ebdad01b88d6b1e7db5b7de8b56042387a Author: Jul <jul@9o.is> Date: Mon, 4 Aug 2014 15:22:39 -0700 now handles transaction state and recovery Diffstat:
20 files changed, 353 insertions(+), 125 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -64,3 +64,11 @@ bower_components/ _SpecRunner.html sbt-linuxlab.sh + +#embedded databases +*.db + +# backups and nohup +*~ +*.out +*.log +\ No newline at end of file diff --git a/project/Build.scala b/project/Build.scala @@ -10,6 +10,7 @@ object LiftProjectBuild extends Build { .settings(libraryDependencies ++= Seq( "net.liftweb" %% "lift-webkit" % Ver.lift, + "net.liftweb" %% "lift-mapper" % Ver.lift, "net.liftmodules" %% ("extras_"+Ver.lift_edition) % "0.4-SNAPSHOT", "com.typesafe.akka" %% "akka-actor" % "2.3.3", "com.typesafe.akka" %% "akka-agent" % "2.3.3", @@ -23,6 +24,7 @@ object LiftProjectBuild extends Build { //"io.kamon" %% "kamon-system-metrics" % "0.3.2", //"io.kamon" %% "kamon-statsd" % "0.3.2", "org.eclipse.jetty" % "jetty-webapp" % Ver.jetty % "container", + "com.h2database" % "h2" % "1.2.138", "ch.qos.logback" % "logback-classic" % "1.0.13", "org.scalatest" %% "scalatest" % "1.9.2" % "test" ) diff --git a/src/main/resources/props/default.props b/src/main/resources/props/default.props @@ -1,2 +1,5 @@ # The machine's unique ID should be given as a param during execution. -# chimera.id -\ No newline at end of file +# chimera.id + +db.user= +db.password= +\ No newline at end of file diff --git a/src/main/resources/props/production.default.props b/src/main/resources/props/production.default.props @@ -4,3 +4,6 @@ mail.smtp.user=AKIAJOIUNHTNJUNRHFBA mail.smtp.pass=AspsC35SzgkewqUG7tQt3e5owiFRPxtAJw3H8+HYoGl1 mail.smtp.port=587 mail.smtp.auth=true + +db.user= +db.password= diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -4,12 +4,14 @@ import net.liftweb._ import actor.LiftActor import common._ import http._ +import mapper._ import util._ import Helpers._ import net.liftmodules.extras.LiftExtras import net.liftweb.sitemap._ import Loc._ import inc.pyc.chimera._ +import model._ import config._ import bitcoin._ import PriceTicker.commands._ @@ -25,6 +27,9 @@ class Boot extends Loggable { // where to search snippet LiftRules.addToPackages("inc.pyc.chimera") + + // set up Local DB + this.setDB() // set the default htmlProperties LiftRules.htmlProperties.default.set((r: Req) => new Html5Properties(r.userAgent)) @@ -69,4 +74,34 @@ class Boot extends Loggable { PriceTicker ! Tick Wallet ! InitBalance } + + def setDB(): Unit = { + + // set up connection + if (!DB.jndiJdbcConnAvailable_?) { + val vendor = + new StandardDBVendor(Props.get("db.driver") openOr "org.h2.Driver", + Props.get("db.url") openOr + "jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE", + Props.get("db.user"), Props.get("db.password")) + + LiftRules.unloadHooks.append(vendor.closeAllConnections_! _) + + DB.defineConnectionManager(util.DefaultConnectionIdentifier, vendor) + } + + // setup schemas + Schemifier.schemify(true, Schemifier.infoF _, Transaction) + + // setup H2 login + // H2 Console + if (Props.devMode || Props.testMode) { + LiftRules.liftRequest.append({ + case r if (r.path.partPath match { + case "console" :: _ => true + case _ => false + }) => false + }) + } + } } diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/BitcoinServices.scala b/src/main/scala/inc/pyc/chimera/bitcoin/BitcoinServices.scala @@ -66,5 +66,5 @@ object BitcoinServices { */ class LookupBitcoinServices extends LiftActorEventBus { // newPrice - override protected def mapSize: Int = 100 + 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 @@ -43,7 +43,10 @@ object PriceTicker { /** * Price per bitcoin. */ - case class Price(price: String) + 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. @@ -63,7 +66,7 @@ object PriceTicker { case object Tick } - def price(): Future[Double] = ((actor ? GetPrice).mapTo[Price]).map(_.price.toDouble) + def price(): Future[Price] = (actor ? GetPrice).mapTo[Price] } /** diff --git a/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinPriceTicker.scala b/src/main/scala/inc/pyc/chimera/bitcoin/service/BitcoinPriceTicker.scala @@ -26,11 +26,8 @@ trait HttpBitcoinPriceTicker extends HttpBitcoinService with BitcoinPriceTicker val priceTicker: Receive = { case Tick => - val price = formatPrice(buyPrice) - log.info("New Price: {}", price) - sender ! Price(price) + val price = Price(buyPrice) + log.info("New Price: {}", price.format) + sender ! price } - - private def formatPrice(price: String): String = - "%1.2f" format (price.toDouble) } \ 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 @@ -58,7 +58,7 @@ object BitcoinJsonRPC { category: String, amount: BigDecimal, confirmations: BigDecimal, timereceived: BigDecimal) // OTHER BITCOIN MESSAGES - case class AddressValidation(isvalid: Boolean, address: Option[String], + case class AddressValidation(isvalid: Boolean, address: String, ismine: Option[Boolean], pubkey: Option[String], iscompressed: Option[Boolean]) // ACTOR MESSAGES diff --git a/src/main/scala/inc/pyc/chimera/config/Machine.scala b/src/main/scala/inc/pyc/chimera/config/Machine.scala @@ -5,12 +5,12 @@ import lycia._ import akka.agent._ import akka.actor.ActorSystem import concurrent._ -import ExecutionContext.Implicits.global import net.liftweb.util.Props object Machine { implicit val system = ActorSystem("MachineConfig") + import system.dispatcher val id = Agent(Props.get("chimera.id", "default")) val name = Agent(Lycia.machineName) diff --git a/src/main/scala/inc/pyc/chimera/model/Transaction.scala b/src/main/scala/inc/pyc/chimera/model/Transaction.scala @@ -0,0 +1,116 @@ +package inc.pyc.chimera +package model + +import net.liftweb._ +import util._ +import mapper._ +import json._ +import JsonDSL._ +import java.util.Date + +/** + * The information of the current customer transaction. + */ +class Transaction extends KeyedMapper[String, Transaction] { + def getSingleton = Transaction + def primaryKeyField = address + + /** + * Bitcoin address to send bitcoin + */ + object address extends MappedStringIndex(this, 256) { + override def writePermission_? = true + override def dbAutogenerated_? = false + override def dbNotNull_? = true + } + + /** + * Accepted static buy price + */ + object price extends MappedString(this, 256) + + /** + * Buy limit (in USD) + */ + object limit extends MappedDouble(this) + + /** + * Whether we ever scanned the address before + */ + object newAddress extends MappedBoolean(this) + + /** + * The USD bills the user has inserted into the bill acceptor + */ + object billsInserted extends MappedString(this, 2048) { + def asList: List[Int] = + if(get.isEmpty) Nil + else get.split(",").map(_.toInt).toList + } + + /** + * Whether the bitcoin were sent or not + */ + object sent extends MappedBoolean(this) + + /** + * Time this transaction started + */ + object startTime extends MappedDateTime(this) { + override def defaultValue = new Date() + } + + /** + * Time this transaction stopped + */ + object stopTime extends MappedDateTime(this) { + def option: Option[Date] = + if(get == null) None + else Some(get) + + def optionString: Option[String] = option.map(_.toString) + } + + /** + * The wizard page the user is currently seeing. + */ + object currentPage extends MappedString(this, 256) + + /** + * Registered user's first name + */ + object fname extends MappedString(this, 256) + + /** + * Registered user's last name + */ + object lname extends MappedString(this, 256) + + /** + * Registered user's email address + */ + object email extends MappedEmail(this, 256) + + def asJValue: JValue = + (address.name -> address.get) ~ + (price.name -> price.get) ~ + (limit.name -> limit.get) ~ + (newAddress.name -> newAddress.get) ~ + (billsInserted.name -> billsInserted.asList) ~ + (sent.name -> sent.get) ~ + (startTime.name -> startTime.get.toString) ~ + (stopTime.name -> stopTime.optionString) ~ + (currentPage.name -> currentPage.get) ~ + (fname.name -> option(fname.get)) ~ + (lname.name -> option(lname.get)) ~ + (email.name -> option(email.get)) + + + def option(get: String): Option[String] = + if(get isEmpty) None + else Some(get) + +} + +object Transaction extends Transaction + with KeyedMetaMapper[String, Transaction] +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/BitcoinAddressScanner.scala b/src/main/scala/inc/pyc/chimera/snippet/BitcoinAddressScanner.scala @@ -0,0 +1,57 @@ +package inc.pyc.chimera +package snippet + +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 + + +class BitcoinAddressScanner extends RoundTripSnippet with Logger { + + 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() + } yield + if (!validation.isvalid) { + Message("failure") + } else { + val address = validation.address + val tx = + Transaction.create + .address(address) + .price(price.format) + .limit(Lycia.buyLimit) + .currentPage("Insert Bills") + + currentTransaction send tx + + Future { + // TODO check website database if address exists + // TODO check if address is owned by a user + + // load any existing incomplete transactions + Transaction find(address) map { + tx => + val ctx = currentTransaction() + if(ctx.billsInserted.asList.isEmpty) { + currentTransaction send tx + txBus.publish(EventUpdate(topics.transactionUpdate, tx.asJValue)) + } + } + } + + Message("success", data = Some(tx.asJValue)) + } +} +\ 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 @@ -0,0 +1,30 @@ +package inc.pyc.chimera +package snippet + +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 ! 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/PriceTickerComet.scala b/src/main/scala/inc/pyc/chimera/snippet/PriceTickerComet.scala @@ -1,68 +0,0 @@ -package inc.pyc.chimera -package snippet - -import bitcoin._ -import lycia._ -import PriceTicker.commands._ -import BitcoinServices.system.dispatcher -import Wallet.commands._ -import service._ -import xml._ -import net.liftweb._ -import http._ -import json.JsonAST._ -import json.JsonDSL._ -import akka.actor.ActorSystem -import net.liftweb.common.Logger -//import kamon.trace.TraceRecorder - -/** - * 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 ! GossipPrice - } - - override def render(o: NodeSeq): NodeSeq = { - super.render(o) - } -} - -class BitcoinAddressScanner extends RoundTripSnippet with Logger { - - def roundTrips: List[RoundTripInfo] = List("submitAddress" -> submitAddress _) - - def submitAddress(json: JValue): Message = - for (JString(address) <- json) yield - for { - validation <- Wallet.validateAddress(address) - } yield { - - // TODO check website database if address exists - // TODO check if address is owned by a user - - - CurrentTransaction.set(Some(Transaction( - address = "blahh", - limit = Lycia.buyLimit, - price = 500.23, - currentPage = "Insert Bills" - ))) - info("Tran: "+CurrentTransaction) - - - - if(validation.isvalid) Message("success", transaction = CurrentTransaction) - else Message("failure") - } -} -\ 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,54 +1,66 @@ package inc.pyc.chimera package snippet -import org.joda.time.DateTime -import net.liftweb.json.JsonAST.JArray -import net.liftweb.http.RequestVar +import xml._ +import model._ +import akka.actor._ +import akka.agent._ -object CurrentTransaction extends RequestVar[Option[Transaction]](None) +object TransactionState { + import InternalTransactionState._ + import system.dispatcher + + val currentTransaction: Agent[Transaction] = Agent(Transaction.create) + + /** + * Event Bus for Transaction State. + */ + val txBus = new LookupTransaction + + /** + * Subscription topics for event bus. + */ + object topics { + val transactionUpdate = "transactionUpdate" + } +} /** - * Information about the user. - * - * @param fname First Name - * @param lname Last Name - * @param email Email Address - * @param limit Buy Limit + * Handling state of the current transaction. */ -case class UserInfo( - fname: String, - lname: String, - email: String, - limit: Double) +private object InternalTransactionState { + + /** + * Transaction State Actor System + */ + implicit val system = ActorSystem("TransactionSystem") +} /** - * The information of the current customer transaction. - * - * @param address Bitcoin address to send bitcoin - * @param price Accepted static buy price - * @param limit Buy limit (in USD) - * @param newAddress Whether we ever scanned the address before - * @param user Information about the user if they're registered - * @param billsInserted The USD bills the user has inserted into the bill acceptor - * @param sent Whether the bitcoin were sent or not - * @param startTime Time this transaction started - * @param stopTime Time this transaction stopped - * @param currentPage The wizard page the user is currently seeing. + * PubSub for transaction updates. */ -case class Transaction( - address: String, - price: Double, - limit: Double, - newAddress: Boolean = true, - user: Option[UserInfo] = None, - billsInserted: JArray = JArray(Nil), - sent: Boolean = false, - startTime: DateTime = DateTime.now, - stopTime: Option[DateTime] = None, - currentPage: String -) { +class LookupTransaction extends LiftActorEventBus { + // transactionUpdate + override protected def mapSize: Int = 1 +} + +/** + * Snippet to update the current transaction. + */ +class TransactionComet extends EventRegister { + + implicit val system = InternalTransactionState.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 @@ -26,7 +26,8 @@ trait ImplicitSnip { /* * This is used to send messages to client. */ -case class Message(msg_type: String, msg: Option[String] = None, transaction: Option[Transaction] = None) +case class Message(msg_type: String, msg: Option[String] = None, data: Option[JValue] = None) + object Message { def fail = Message("failure") } \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml @@ -18,4 +18,15 @@ <url-pattern>/*</url-pattern> </filter-mapping> +<servlet> + <servlet-name>H2Console</servlet-name> + <servlet-class>org.h2.server.web.WebServlet</servlet-class> + <load-on-startup>0</load-on-startup> +</servlet> + +<servlet-mapping> + <servlet-name>H2Console</servlet-name> + <url-pattern>/console/*</url-pattern> +</servlet-mapping> + </web-app> diff --git a/src/main/webapp/app/ActorsBridge.js b/src/main/webapp/app/ActorsBridge.js @@ -25,4 +25,11 @@ window.ActorsBridge = function(sendFunc) { self.broadcast('newPrice', message); }; + /** + * Updates the state of the current transaction. + */ + self.transactionUpdate = function(message) { + self.broadcast('transactionUpdate', message); + }; + }; \ No newline at end of file diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -35,9 +35,12 @@ app.controller('PriceTickerCtrl', ['$scope', '$rootScope', function($scope, $roo app.controller('MainCtrl', ['$scope', '$rootScope', function($scope, $rootScope) { - + $rootScope.transaction = {}; + $rootScope.$on("transactionUpdate", function (event, message) { + jQuery.extend($rootScope.transaction, message); + }); }]); @@ -68,8 +71,12 @@ app.controller('WalletScannerCtrl', ['$scope', '$rootScope', '$controller', '$ti scan_sound.play(); var success = function(data) { - window.console.log("Data: "+data.data); - $rootScope.transaction = data.data; + + // set default transaction if it hasn't been set yet + if($rootScope.transaction.address === undefined) { + $rootScope.transaction = data.data; + } + WizardHandler.wizard().next(); }; diff --git a/src/main/webapp/templates-hidden/base-wrap.html b/src/main/webapp/templates-hidden/base-wrap.html @@ -37,6 +37,7 @@ <!-- Comets --> <script data-lift="PriceTickerComet"></script> +<script data-lift="TransactionComet"></script> </body> </html>