bitcoin-client
bitcoin client library for price ticker and wallet
git clone https://9o.is/git/bitcoin-client.git
commit f9d7c4f992646d6f5687756069781c1432e733cd Author: Jul <jul@9o.is> Date: Fri, 3 Oct 2014 16:42:04 -0400 init Diffstat:
| A | .gitignore | | | 66 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | README.md | | | 4 | ++++ |
| A | project/Build.scala | | | 19 | +++++++++++++++++++ |
| A | project/BuildSettings.scala | | | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | project/plugins.sbt | | | 14 | ++++++++++++++ |
| A | project/sbt-launch.jar | | | 0 | |
| A | sbt.sh | | | 2 | ++ |
| A | src/main/scala/inc/pyc/bitcoin/BitcoinActorInterface.scala | | | 42 | ++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/BitcoinService.scala | | | 44 | ++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/Settings.scala | | | 20 | ++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/exchange/BitcoinExchange.scala | | | 22 | ++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/provider/BitStamp.scala | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/provider/BlockChain.scala | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/provider/BtcWallet.scala | | | 34 | ++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/service/HttpBitcoinService.scala | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/service/JsonRPC.scala | | | 136 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/service/WsBitcoinService.scala | | | 227 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/ticker/BitcoinPriceTicker.scala | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/ticker/Commands.scala | | | 30 | ++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/ticker/Helper.scala | | | 54 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/ticker/PriceTicker.scala | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/wallet/BitcoinWallet.scala | | | 233 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/wallet/Commands.scala | | | 13 | +++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/wallet/Helper.scala | | | 74 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/wallet/Wallet.scala | | | 75 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
25 files changed, 1400 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -0,0 +1,66 @@ +# use glob syntax. +syntax: glob +*.swp +*.ser +*.class +*~ +*.bak +*.off +*.old +.DS_Store +.cache + +# logs +derby.log + +# eclipse conf file +.settings +.classpath +.project +.manager +.externalToolBuilders + +# ensime/emacs conf files +.ensime +.scala_dependencies + +# building +target +null +tmp* +dist +test-output + +# sbt +target +lib_managed +src_managed +project/boot +project/plugins/project + +# other scm +.svn +.CVS +.hg* + +# switch to regexp syntax. +# syntax: regexp +# ^\.pc/ + +# IntelliJ +*.eml +*.iml +*.ipr +*.iws +.idea + +# Pax Runner (for easy OSGi launching) +runner + +#grunt/requirejs stuff +node_modules/ +bower_components/ +.grunt/ +_SpecRunner.html + +sbt-linuxlab.sh diff --git a/README.md b/README.md @@ -0,0 +1,4 @@ +Bitcoin Library +==================== + +Provides several client-side bitcoin services such as price ticker, wallet and more. diff --git a/project/Build.scala b/project/Build.scala @@ -0,0 +1,19 @@ +import sbt._ +import sbt.Keys._ + +object LiftProjectBuild extends Build { + + import BuildSettings._ + + lazy val root = Project("bitcoin", file(".")) + .settings(appSettings: _*) + .settings(libraryDependencies ++= + Seq( + "com.typesafe.akka" %% "akka-actor" % "2.3.3", + "net.databinder.dispatch" %% "dispatch-lift-json" % "0.11.0", + "com.palletops" % "java-websocket" % "1.3.1-SNAPSHOT", + "ch.qos.logback" % "logback-classic" % "1.0.13" % "compile", + "org.scalatest" %% "scalatest" % "1.9.2" % "test" + ) + ) +} diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala @@ -0,0 +1,58 @@ +import sbt._ +import sbt.Keys._ + +import sbtbuildinfo.Plugin._ +import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys +import ohnosequences.sbt.SbtS3Resolver._ + +object BuildSettings { + val buildTime = SettingKey[String]("build-time") + + val defaultScalaVersion = "2.10.4" + + val basicSettings = Defaults.defaultSettings ++ Seq( + name := "bitcoin", + version := "0.1-SNAPSHOT", + organization := "inc.pyc", + scalaVersion := defaultScalaVersion, + scalacOptions <<= scalaVersion map { sv: String => + if (sv.startsWith("2.10.")) + Seq("-deprecation", "-unchecked", "-feature", "-language:postfixOps", "-language:implicitConversions") + else + Seq("-deprecation", "-unchecked") + }, + resolvers ++= Seq[Resolver]( + "Sonatype Releases" at "http://oss.sonatype.org/content/repositories/releases", + "clojars.org" at "http://clojars.org/repo" + ) + ) + + val appSettings = + basicSettings ++ + S3Resolver.defaults ++ + buildInfoSettings ++ + seq( + buildTime := System.currentTimeMillis.toString, + + // build-info + buildInfoKeys ++= Seq[BuildInfoKey](buildTime), + buildInfoPackage := "inc.pyc", + sourceGenerators in Compile <+= buildInfo, + + // eclipse + EclipseKeys.withSource := true, + + publishMavenStyle := true, + + publishTo := Some(s3resolver.value( + "My "+{if (isSnapshot.value) "snapshots-pyc-inc" else "releases-pyc-inc"}+" S3 bucket", + s3(if (isSnapshot.value) "snapshots-pyc-inc" else "releases-pyc-inc"))), + + s3credentials := { + Path.userHome / ".ivy2" / ".s3credentials" + }, + + s3region := com.amazonaws.services.s3.model.Region.US_Standard + ) +} + diff --git a/project/plugins.sbt b/project/plugins.sbt @@ -0,0 +1,14 @@ +resolvers += Resolver.url( + "bintray-sbt-plugin-releases", + url("http://dl.bintray.com/content/sbt/sbt-plugin-releases"))( + Resolver.ivyStylePatterns) + +resolvers += "softprops-maven" at "http://dl.bintray.com/content/softprops/maven" + +resolvers += "Era7 maven releases" at "http://releases.era7.com.s3.amazonaws.com" + +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.2.5") + +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0") + +addSbtPlugin("ohnosequences" % "sbt-s3-resolver" % "0.10.1") diff --git a/project/sbt-launch.jar b/project/sbt-launch.jar Binary files differ. diff --git a/sbt.sh b/sbt.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -Dfile.encoding=UTF8 -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=384M -jar `dirname $0`/project/sbt-launch.jar "$@" diff --git a/src/main/scala/inc/pyc/bitcoin/BitcoinActorInterface.scala b/src/main/scala/inc/pyc/bitcoin/BitcoinActorInterface.scala @@ -0,0 +1,41 @@ +package inc.pyc.bitcoin + +import akka.actor._ + +private[bitcoin] abstract class BitcoinActorInterface(service: BitcoinService.Value) + extends Actor with ActorLogging { + + override def preStart { + val props = BitcoinService.props(service) + val ref = context.actorOf(props) + context become execute(ref) + context watch ref + } + + /** + * Handle service with this receive function. + */ + def handle(service: ActorRef): Receive + + def receive = { + case _ => + } + + def execute(service: ActorRef): Receive = + change(service) orElse handle(service) + + /** + * Allows user to change bitcoin service provider. + */ + def change(service: ActorRef): Receive = { + case ChangeBitcoinService(provider) => + if (context.child(provider.toString).isEmpty) { + val prop = BitcoinService.props(provider) + val ref = context actorOf(prop) + context become execute(ref) + context watch ref + context unwatch service + context stop service + } + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/BitcoinService.scala b/src/main/scala/inc/pyc/bitcoin/BitcoinService.scala @@ -0,0 +1,44 @@ +package inc.pyc.bitcoin + +import service._ +import akka.actor.Props +import inc.pyc.bitcoin.provider.BtcWallet +import inc.pyc.bitcoin.provider.BlockChain +import inc.pyc.bitcoin.provider.BitStamp + +/** + * The different choices of bitcoin services. + */ +object BitcoinService extends Enumeration { + type BitcoinService = Value + val BitStamp, BlockChain, BtcWallet = Value + + def props(serv: BitcoinService): Props = props(fqcn(serv)) + def props(serv: BitcoinService, name: String): Props = props(fqcn(serv), name) + def props(fqcn: String): Props = Props(Class forName fqcn) + def props(fqcn: String, name: String): Props = Props(Class forName fqcn, name) + + /** + * All bitcoin services that have a price ticker. + */ + def priceTickers: List[BitcoinService] = List( + BitStamp, BlockChain) + + /** + * All bitcoin services that have a wallet. + */ + def wallets: List[BitcoinService] = List( + BtcWallet, BlockChain) + + /* + * Creates FQCN given the `BitcoinService` value. + */ + private def fqcn(serv: BitcoinService) = + "inc.pyc.bitcoin.provider."+serv.toString() +} + + +/** + * Command to change the bitcoin service provider. + */ +case class ChangeBitcoinService(service: BitcoinService.Value) diff --git a/src/main/scala/inc/pyc/bitcoin/Settings.scala b/src/main/scala/inc/pyc/bitcoin/Settings.scala @@ -0,0 +1,20 @@ +package inc.pyc.bitcoin + +import akka.actor._ +import com.typesafe.config._ + +class SettingsImpl(config: Config) extends Extension { + val bitcoin = config getConfig "bitcoin" +} + +object Settings extends ExtensionId[SettingsImpl] + with ExtensionIdProvider { + + override def lookup = Settings + + override def createExtension(system: ExtendedActorSystem) = + new SettingsImpl(system.settings.config) + + override def get(system: ActorSystem): SettingsImpl = super.get(system) +} + diff --git a/src/main/scala/inc/pyc/bitcoin/exchange/BitcoinExchange.scala b/src/main/scala/inc/pyc/bitcoin/exchange/BitcoinExchange.scala @@ -0,0 +1,21 @@ +package inc.pyc.bitcoin +package exchange + +import akka.actor._ +import inc.pyc.bitcoin.ticker.HttpBitcoinPriceTicker +import inc.pyc.bitcoin.service.HttpBitcoinService + +/** + * 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/provider/BitStamp.scala b/src/main/scala/inc/pyc/bitcoin/provider/BitStamp.scala @@ -0,0 +1,56 @@ +package inc.pyc.bitcoin +package provider + +import exchange._ +import dispatch._ +import akka.actor._ +import net.liftweb.json._ + + +/** + * BitStamp REST services + */ +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/provider/BlockChain.scala b/src/main/scala/inc/pyc/bitcoin/provider/BlockChain.scala @@ -0,0 +1,42 @@ +package inc.pyc.bitcoin +package provider + +import ticker._ +import wallet._ +import dispatch._ +import net.liftweb.json._ +import akka.actor._ + + +/** + * BlockChain REST services + */ +class BlockChain extends Actor + with ActorLogging + with HttpBitcoinPriceTicker + with HttpBitcoinWallet { + + + def receive = bitcoinWallet orElse priceTicker + + + protected val config = Settings(context.system).bitcoin.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 = { + val usd = request(ticker_api) \ "USD" + compact(render(usd \ "last")) + } + + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/provider/BtcWallet.scala b/src/main/scala/inc/pyc/bitcoin/provider/BtcWallet.scala @@ -0,0 +1,33 @@ +package inc.pyc.bitcoin +package provider + +import wallet._ +import net.liftweb.json._ +import akka.actor._ + + +// TODO implement + +class BtcWallet extends Actor + with ActorLogging + with WssBitcoinWallet { + + + val config = Settings(context.system).bitcoin.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,42 @@ +package inc.pyc.bitcoin +package service + +import dispatch._ +import net.liftweb.json.JsonAST.JValue +import akka.actor._ + +/** + * Bitcoin service over HTTP communications. + */ +trait HttpBitcoinService { + 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,226 @@ +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 { + 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: ${getClass.getSimpleName} 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 ${getClass.getSimpleName} 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: {}", getClass.getSimpleName, 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", getClass.getSimpleName) + self ! Connected + } else { + new ConnectException(s"${getClass.getSimpleName} 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/bitcoin/ticker/BitcoinPriceTicker.scala b/src/main/scala/inc/pyc/bitcoin/ticker/BitcoinPriceTicker.scala @@ -0,0 +1,39 @@ +package inc.pyc.bitcoin +package ticker + +import 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 => + + + /** + * 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) + sender ! price + } + + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/ticker/Commands.scala b/src/main/scala/inc/pyc/bitcoin/ticker/Commands.scala @@ -0,0 +1,29 @@ +package inc.pyc.bitcoin +package ticker + +/** + * Price per bitcoin. + * @param price price of one bitcoin + */ +case class Price(price: String) { + def format: String = "%,1.2f" format (price.toDouble) + def double: Double = price.toDouble +} + +/** + * Command to send 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. + * @param profit 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/ticker/Helper.scala b/src/main/scala/inc/pyc/bitcoin/ticker/Helper.scala @@ -0,0 +1,53 @@ +package inc.pyc.bitcoin +package ticker + +import BitcoinService._ +import akka.actor._ +import akka.pattern._ +import akka.util.Timeout +import scala.concurrent._ +import duration._ + +/** + * Price Ticker API implementation + */ +trait PriceTickerHelper { + this: Actor => + + /** + * Price ticker actor reference. BitStamp is the default. + */ + lazy val priceTicker: ActorRef = priceTicker(BitStamp) + + /** + * Creates a price ticker actor + */ + def priceTicker(service: BitcoinService.Value) = + context.system.actorOf(Props(classOf[PriceTicker], service), "PriceTicker") + + /** + * Get current set price. + */ + def priceFuture = ask(priceTicker, GetPrice)(1 second).mapTo[Price] + + /** + * Get current set price. + */ + def price: Price = Await.result(priceFuture, 500 millis) + + /** + * Update price. + */ + def tick = priceTicker ! Tick + + /** + * Set percentage over market price. + */ + def percentage(profit: Double) = priceTicker ! Percentage(profit) + + /** + * Change bitcoin service provider. + */ + def changeBitcoinService(service: BitcoinService.Value) = + priceTicker ! ChangeBitcoinService(service) +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/ticker/PriceTicker.scala b/src/main/scala/inc/pyc/bitcoin/ticker/PriceTicker.scala @@ -0,0 +1,50 @@ +package inc.pyc.bitcoin +package ticker + +import akka.actor._ +import akka.pattern._ +import akka.util.Timeout +import akka.actor.SupervisorStrategy._ +import scala.concurrent._ +import duration._ + + +/** + * Main Price Ticker actor that creates child actor and uses the chosen, + * configured service to get the price per bitcoin. + */ +class PriceTicker(service: BitcoinService.Value) extends BitcoinActorInterface(service) { + + override val supervisorStrategy = + OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { + case _: java.net.ConnectException => Resume + case t => + super.supervisorStrategy.decider. + applyOrElse(t, (_: Any) => Escalate) + } + + implicit val timeout = Timeout(4 seconds) + + /** Price per bitcoin */ + private var price: String = "0.00" + + /** Percentage price over market */ + private var percentage: Double = 0 + + def handle(service: ActorRef): Receive = { + case Tick => service ! Tick + case GetPrice => sender ! priceWithPercentage + case Percentage(profit) => percentage = profit + case Price(newPrice) => price = newPrice + case msg => log warning ("unhandled message {}", msg) + } + + /** + * Formats the market price plus percentage. + */ + def priceWithPercentage: Price = { + val p = price.toDouble + val withPercentage = p + (p * (percentage * 0.01)) + Price(withPercentage toString) + } +} diff --git a/src/main/scala/inc/pyc/bitcoin/wallet/BitcoinWallet.scala b/src/main/scala/inc/pyc/bitcoin/wallet/BitcoinWallet.scala @@ -0,0 +1,232 @@ +package inc.pyc.bitcoin +package wallet + +import service._ +import JsonRPC._ +import BitcoinJsonRPC._ +import net.liftweb.json._ +import net.liftweb.json.JsonAST.JValue +import scala.collection._ +import scala.concurrent._ +import duration._ +import akka.util.Timeout +import com.typesafe.config.Config +import akka.actor._ +import akka.util.Timeout.durationToTimeout +import dispatch._ +import scala.util.Try + +/** + * Actor to handle bitcoin wallet communications with JSON-RPC. + */ +sealed trait BitcoinWallet { + 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 => + + + /** + * Handles notifications incoming from server. + */ + protected def handleNotification: Receive + + + override val api = new java.net.URI(walletUri) + + + 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) => + Try(params(1).extract[TransactionNotification]).filter (_.category == "receive"). + foreach (tx => 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) + Try(msg.extract[JsonNotification]).foreach( + handleResponseNotification.applyOrElse(_, unhandled)) + else + Try(msg.extract[JsonResponse]).foreach(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/wallet/Commands.scala b/src/main/scala/inc/pyc/bitcoin/wallet/Commands.scala @@ -0,0 +1,12 @@ +package inc.pyc.bitcoin +package wallet + +/** + * Balance remaining in bitcoin wallet. + */ +case class Balance(remaining: Double) + +/** + * Initializes the balance in wallet + */ +case object InitBalance +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/wallet/Helper.scala b/src/main/scala/inc/pyc/bitcoin/wallet/Helper.scala @@ -0,0 +1,73 @@ +package inc.pyc.bitcoin +package wallet + +import BitcoinService._ +import service._ +import BitcoinJsonRPC._ +import akka.actor._ +import akka.pattern._ +import akka.util.Timeout +import scala.concurrent._ +import duration._ + + +/** + * Wallet API implementation + */ +trait WalletHelper { + this: Actor => + + implicit private val timeout: Timeout = Timeout(5 seconds) + + /** + * Wallet actor reference. BlockChain is the default. + */ + lazy val wallet: ActorRef = wallet(BlockChain) + + /** + * Creates a wallet actor + */ + def wallet(service: BitcoinService.Value) = + context.system.actorOf(Props(classOf[Wallet], service), "BitcoinWallet") + + def initBalance: Unit = + wallet ! InitBalance + + + def createRawTransaction(inputs: Seq[(String, BigDecimal)], receivers: Seq[(String, BigDecimal)]) = + (wallet ? CreateRawTransaction(inputs, receivers)).mapTo[String] + + + def listUnspentTransactions(minConfirmations: BigDecimal = 1, maxConfirmations: BigDecimal = 999999) = + (wallet ? ListUnspentTransactions(minConfirmations, maxConfirmations)).mapTo[Seq[UnspentTransaction]] + + + def sendRawTransaction(signedTransaction: String) = + (wallet ? SendRawTransaction(signedTransaction)).mapTo[String] + + + def getBalance: Future[String] = + (wallet ? GetBalance).mapTo[String] + + + def getNewAddress: Future[String] = + (wallet ? GetNewAddress).mapTo[String] + + + def getRawTransaction(transactionHash: String) = + (wallet ? GetRawTransaction(transactionHash)).mapTo[RawTransaction] + + + def signRawTransaction(transaction: String): Future[SignedTransaction] = + (wallet ? SignRawTransaction(transaction)).mapTo[SignedTransaction] + + + def validateAddress(address: String): Future[AddressValidation] = + (wallet ? ValidateAddress(address)).mapTo[AddressValidation] + + + def changeBitcoinService(service: BitcoinService.Value) = + wallet ! ChangeBitcoinService(service) + + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/wallet/Wallet.scala b/src/main/scala/inc/pyc/bitcoin/wallet/Wallet.scala @@ -0,0 +1,74 @@ +package inc.pyc.bitcoin +package wallet + +import service._ +import BitcoinJsonRPC._ +import akka.actor._ +import akka.event._ +import akka.util._ +import akka.pattern._ +import akka.actor.SupervisorStrategy._ +import scala.concurrent._ +import duration._ + + +/** + * Main Bitcoin Wallet actor that creates child actor and uses the chosen, + * configured service as the bitcoin wallet. + */ +class Wallet(service: BitcoinService.Value) extends BitcoinActorInterface(service) { + 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) + } + + implicit val timeout = Timeout(5 seconds) + + /** Balance remaining in the wallet */ + private var balance: Double = 0 + + def handle(service: ActorRef): Receive = { + case InitBalance => initializeBalance(service) + case GetBalance => sender ! Balance(balance) + case Balance(newBalance) => balance = newBalance + case ValidateAddress(data) => validateAddress(data, service) + case msg => log warning ("unhandled message {}", msg) + } + + /** + * Sets the balance for the configured wallet. + */ + def initializeBalance(service: ActorRef) { + val newBalance = (service ? GetBalance).mapTo[String] + newBalance map { newBalance => + balance = newBalance.toDouble + } + } + + /** + * Validates a bitcoin address. + * @param data string data that may have been scanned from QR code + */ + def validateAddress(data: String, service: ActorRef) { + val extracted = extractFromBitcoinUri(data) + service ? ValidateAddress(extracted) pipeTo sender + } + + /** + * 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 _ => "" + } + } +} +\ No newline at end of file