bitcoin-client
bitcoin client library for price ticker and wallet
git clone https://9o.is/git/bitcoin-client.git
commit 1173cf9ed01df2b355c8a878ca7c02fb182a29e9 parent 2d72071688252e3557e7792909bf79636efd06d9 Author: Jul <jul@9o.is> Date: Sun, 5 Oct 2014 15:00:51 -0400 refactored a lot Diffstat:
| D | src/main/scala/inc/pyc/bitcoin/BitcoinActorInterface.scala | | | 42 | ------------------------------------------ |
| M | src/main/scala/inc/pyc/bitcoin/BitcoinService.scala | | | 4 | +--- |
| A | src/main/scala/inc/pyc/bitcoin/JsonRPC.scala | | | 137 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/PriceTicker.scala | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/scala/inc/pyc/bitcoin/Wallet.scala | | | 217 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/main/scala/inc/pyc/bitcoin/exchange/BitcoinExchange.scala | | | 13 | +++++-------- |
| M | src/main/scala/inc/pyc/bitcoin/provider/BitStamp.scala | | | 25 | +++++++++---------------- |
| M | src/main/scala/inc/pyc/bitcoin/provider/BlockChain.scala | | | 24 | +++++++----------------- |
| M | src/main/scala/inc/pyc/bitcoin/provider/BtcWallet.scala | | | 21 | +++++++-------------- |
| D | src/main/scala/inc/pyc/bitcoin/service/HttpBitcoinService.scala | | | 41 | ----------------------------------------- |
| A | src/main/scala/inc/pyc/bitcoin/service/HttpService.scala | | | 41 | +++++++++++++++++++++++++++++++++++++++++ |
| D | src/main/scala/inc/pyc/bitcoin/service/JsonRPC.scala | | | 136 | ------------------------------------------------------------------------------- |
| D | src/main/scala/inc/pyc/bitcoin/service/WsBitcoinService.scala | | | 227 | ------------------------------------------------------------------------------- |
| A | src/main/scala/inc/pyc/bitcoin/service/WsService.scala | | | 206 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | src/main/scala/inc/pyc/bitcoin/ticker/BitcoinPriceTicker.scala | | | 40 | ---------------------------------------- |
| D | src/main/scala/inc/pyc/bitcoin/ticker/Commands.scala | | | 30 | ------------------------------ |
| D | src/main/scala/inc/pyc/bitcoin/ticker/Helper.scala | | | 54 | ------------------------------------------------------ |
| D | src/main/scala/inc/pyc/bitcoin/ticker/PriceTicker.scala | | | 48 | ------------------------------------------------ |
| D | src/main/scala/inc/pyc/bitcoin/wallet/BitcoinWallet.scala | | | 233 | ------------------------------------------------------------------------------- |
| D | src/main/scala/inc/pyc/bitcoin/wallet/Commands.scala | | | 14 | -------------- |
| D | src/main/scala/inc/pyc/bitcoin/wallet/Helper.scala | | | 73 | ------------------------------------------------------------------------- |
| D | src/main/scala/inc/pyc/bitcoin/wallet/Wallet.scala | | | 74 | -------------------------------------------------------------------------- |
22 files changed, 687 insertions(+), 1070 deletions(-)
diff --git a/src/main/scala/inc/pyc/bitcoin/BitcoinActorInterface.scala b/src/main/scala/inc/pyc/bitcoin/BitcoinActorInterface.scala @@ -1,41 +0,0 @@ -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 @@ -2,9 +2,7 @@ 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 +import provider._ /** * The different choices of bitcoin services. diff --git a/src/main/scala/inc/pyc/bitcoin/JsonRPC.scala b/src/main/scala/inc/pyc/bitcoin/JsonRPC.scala @@ -0,0 +1,136 @@ +package inc.pyc.bitcoin + +import net.liftweb._ +import net.liftweb.json.JsonAST._ +import scala.math.BigDecimal.int2bigDecimal +import scala.math.BigInt.int2bigInt + +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/PriceTicker.scala b/src/main/scala/inc/pyc/bitcoin/PriceTicker.scala @@ -0,0 +1,56 @@ +package inc.pyc.bitcoin + +import service._ +import dispatch.Req +import akka.actor._ + +/** + * A Bitcoin service with buy/sell prices. + */ +sealed trait PriceTicker + +private[bitcoin] trait HttpPriceTicker extends HttpService with PriceTicker { + 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 + } +} + +/** + * Command to update the price per bitcoin. + * Sent frequently to update the price in the UI. + */ +case object Tick + +/** + * Price per bitcoin. + * @param price price of one bitcoin + */ +case class Price(price: Double, percentage: Double = 0) { + def format: String = "%,1.2f" format priceWithPercentage + + /** + * Formats the over market price with percentage. + */ + def priceWithPercentage: Double = { + price + (price * (percentage * 0.01)) + } +} + +object Price { + def apply(s: String): Price = Price(s.toDouble) +} +\ 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,216 @@ +package inc.pyc.bitcoin + +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 Wallet { + 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. + */ +private[bitcoin] trait HttpWallet extends HttpService with Wallet { + 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. + */ +private[bitcoin] trait WsWallet extends WsService with Wallet { + 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. + */ +private[bitcoin] trait WssWallet extends WssService with WsWallet { + this: Actor with ActorLogging => +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/exchange/BitcoinExchange.scala b/src/main/scala/inc/pyc/bitcoin/exchange/BitcoinExchange.scala @@ -1,21 +1,18 @@ package inc.pyc.bitcoin package exchange +import service._ 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 - - +sealed trait Exchange /** * Bitcoin Exchange service to buy and sell bitcoin via HTTP */ -trait HttpBitcoinExchange - extends HttpBitcoinService with HttpBitcoinPriceTicker { - this: Actor with ActorLogging => +trait HttpExchange + extends HttpService with HttpPriceTicker { + 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 @@ -6,34 +6,27 @@ import dispatch._ import akka.actor._ import net.liftweb.json._ - /** * BitStamp REST services */ -class BitStamp extends Actor +class BitStamp + extends Actor with ActorLogging - with HttpBitcoinExchange { - + with HttpExchange { + 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 { diff --git a/src/main/scala/inc/pyc/bitcoin/provider/BlockChain.scala b/src/main/scala/inc/pyc/bitcoin/provider/BlockChain.scala @@ -1,42 +1,32 @@ 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 { - - + with HttpPriceTicker + with HttpWallet { + 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 @@ -1,18 +1,15 @@ 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 { - - +class BtcWallet extends Actor + with ActorLogging + with WssWallet { + val config = Settings(context.system).bitcoin.getConfig("btcwallet") val walletUri = config.getString("wallet-uri") val rpcUser = config.getString("rpc-user") @@ -20,14 +17,10 @@ class BtcWallet extends Actor 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 _ => + 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 @@ -1,40 +0,0 @@ -package inc.pyc.bitcoin -package service - -import dispatch._ -import Defaults._ -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. - * Some API's like BitStamp will block connection otherwise. - */ - 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 = { - val promise = retry.Backoff(max = 10) { - () => Http.configure(_.setUserAgent(userAgent))(req <:< headers > as.lift.Json).either - } - promise().right.get - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/bitcoin/service/HttpService.scala b/src/main/scala/inc/pyc/bitcoin/service/HttpService.scala @@ -0,0 +1,40 @@ +package inc.pyc.bitcoin +package service + +import dispatch._ +import Defaults._ +import net.liftweb.json.JsonAST.JValue +import akka.actor._ + +/** + * Bitcoin service over HTTP communications. + */ +private[bitcoin] trait HttpService { + 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. + * Some API's like BitStamp will block connection otherwise. + */ + 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 = { + val promise = retry.Backoff(max = 10) { + () => Http.configure(_.setUserAgent(userAgent))(req <:< headers > as.lift.Json).either + } + promise().right.get + } +} +\ 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 @@ -1,135 +0,0 @@ -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 @@ -1,226 +0,0 @@ -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/service/WsService.scala b/src/main/scala/inc/pyc/bitcoin/service/WsService.scala @@ -0,0 +1,205 @@ +package inc.pyc.bitcoin +package service + +import java.net.{ URI, ConnectException } +import concurrent._ +import duration._ +import collection._ +import collection.JavaConversions._ +import akka.actor._ +import akka.pattern._ +import akka.util._ +import net.liftweb.json._ +import net.liftweb.json.JsonAST.JValue +import org.java_websocket._ +import org.java_websocket.client._ +import org.java_websocket.handshake._ +import org.java_websocket.drafts._ +import java.io.File +import java.io.FileInputStream +import java.security.KeyStore +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManagerFactory + +/** + * Bitcoin service over WebSocket communications. + */ +private[bitcoin] trait WsService { + 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. + */ +private[bitcoin] trait WssService extends WsService { + 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 @@ -1,39 +0,0 @@ -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 @@ -1,29 +0,0 @@ -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 @@ -1,53 +0,0 @@ -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 ticker: ActorRef = priceTicker(BitStamp) - - /** - * Creates a price ticker actor - */ - def priceTicker(service: BitcoinService.Value) = - context.system.actorOf(Props(classOf[PriceTicker], service), "Ticker") - - /** - * Get current set price. - */ - def priceFuture = ask(ticker, GetPrice)(1 second).mapTo[Price] - - /** - * Get current set price. - */ - def price: Price = Await.result(priceFuture, 1 second) - - /** - * Update price. - */ - def tick = ticker ! Tick - - /** - * Set percentage over market price. - */ - def percentage(profit: Double) = ticker ! Percentage(profit) - - /** - * Change bitcoin service provider. - */ - def tickerService(service: BitcoinService.Value) = - ticker ! 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 @@ -1,48 +0,0 @@ -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 _: Exception => 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 @@ -1,232 +0,0 @@ -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 @@ -1,13 +0,0 @@ -package inc.pyc.bitcoin -package wallet - -/** - * Balance remaining in bitcoin wallet. - * @param remaining balance remaining in bitcoin - */ -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 @@ -1,72 +0,0 @@ -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), "Wallet") - - 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[Balance] = - (wallet ? GetBalance).mapTo[Balance] - - - 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 walletService(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 @@ -1,73 +0,0 @@ -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 _: Exception => 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