Somewhat dirty standalone server and CLI binary (#558)

* PoC bitcoin-s-cli

* Add CLI, Server sbt projects, remove Ammonite

In this commit we set up sbt configuration for
CLI, Server (in-work-name) and corresponding
test projects.

We also remove Ammonite shell from sbt, as that
isn't really being used. bloop console offers
the same functionality way more ergonimic.

* Move BitcoinSAppConfig into new server project

Server project depends on node, chain wand wallet
so this is a good time for introducing this class
into main sources. We also introduce
BitcoinSTestAppConfig in testkit, to replace the
functionality in BitcoinSAppConfig related to
tests.

* Type chain in blockchainresult

* MVP server setup for node, chain and wallet

* Extremely dirty CLI for interacting with server

* initial attempt at mimicking Bitcoin Core API

* WalletStorage: add method for checking for seed existance

* Check for seed existance on wallet startup

* Fix bug where MnemonicNotFound was not an error

* Segregate confirmed and unconfirmed balance methods

* Add error handling, improve formatting of CLI output

* Tweak build

Bump Sttp version, downgrade to uPickle 2.11 compat,
skip publish in cli-test and server-test

* Add CLI, server and picklers to root project
This commit is contained in:
Torkel Rogstad 2019-07-10 13:33:17 +02:00 committed by Chris Stewart
parent 2632e1a628
commit 30e6d7030f
37 changed files with 1015 additions and 163 deletions

View File

@ -1,3 +1,4 @@
version = "1.5.1"
# See Documentation at https://scalameta.org/scalafmt/#Configuration # See Documentation at https://scalameta.org/scalafmt/#Configuration
maxColumn=80 maxColumn=80
docstrings=ScalaDoc docstrings=ScalaDoc

View File

@ -0,0 +1,3 @@
name := "bitcoin-s-cli-test"
publish / skip := true

9
app/cli/cli.sbt Normal file
View File

@ -0,0 +1,9 @@
name := "bitcoin-s-cli"
libraryDependencies ++= Deps.cli
publish / skip := true
graalVMNativeImageOptions += "-H:EnableURLProtocols=http"
enablePlugins(JavaAppPackaging, GraalVMNativeImagePlugin)

View File

@ -0,0 +1,238 @@
package org.bitcoins.cli
import org.bitcoins.picklers._
import scopt.OParser
import org.bitcoins.core.config.NetworkParameters
import upickle.{default => up}
import CliReaders._
import org.bitcoins.core.protocol._
import org.bitcoins.core.currency._
import org.bitcoins.cli.CliCommand.GetBalance
import org.bitcoins.cli.CliCommand.GetNewAddress
import org.bitcoins.cli.CliCommand.SendToAddress
import org.bitcoins.cli.CliCommand.GetBlockCount
import org.bitcoins.cli.CliCommand.GetBestBlockHash
import org.bitcoins.cli.CliCommand.GetPeers
import org.bitcoins.cli.CliCommand.NoCommand
import java.net.ConnectException
import java.{util => ju}
import ujson.Num
import ujson.Str
case class Config(
command: CliCommand = CliCommand.NoCommand,
network: Option[NetworkParameters] = None,
debug: Boolean = false
)
sealed abstract class CliCommand
object CliCommand {
case object NoCommand extends CliCommand
// Wallet
case class SendToAddress(destination: BitcoinAddress, amount: Bitcoins)
extends CliCommand
case object GetNewAddress extends CliCommand
case object GetBalance extends CliCommand
// Node
case object GetPeers extends CliCommand
// Chain
case object GetBestBlockHash extends CliCommand
case object GetBlockCount extends CliCommand
}
object Cli extends App {
val builder = OParser.builder[Config]
val parser = {
import CliCommand._
import builder._
OParser.sequence(
programName("bitcoin-s-cli"),
opt[NetworkParameters]('n', "network")
.action((np, conf) => conf.copy(network = Some(np)))
.text("Select the active network."),
opt[Unit]("debug")
.action((_, conf) => conf.copy(debug = true))
.text("Print debugging information"),
cmd("getblockcount")
.hidden()
.action((_, conf) => conf.copy(command = GetBlockCount))
.text(s"Get the block height"),
cmd("getbestblockhash")
.hidden()
.action((_, conf) => conf.copy(command = GetBestBlockHash))
.text(s"Get the best block hash"),
cmd("getbalance")
.hidden()
.action((_, conf) => conf.copy(command = GetBalance))
.text("Get the wallet balance"),
cmd("getnewaddress")
.hidden()
.action((_, conf) => conf.copy(command = GetNewAddress))
.text("Get a new address"),
cmd("sendtoaddress")
.hidden()
.action(
// TODO how to handle null here?
(_, conf) => conf.copy(command = SendToAddress(null, 0.bitcoin)))
.text("Send money to the given address")
.children(
opt[BitcoinAddress]("address")
.required()
.action((addr, conf) =>
conf.copy(command = conf.command match {
case send: SendToAddress =>
send.copy(destination = addr)
case other => other
})),
opt[Bitcoins]("amount")
.required()
.action((btc, conf) =>
conf.copy(command = conf.command match {
case send: SendToAddress =>
send.copy(amount = btc)
case other => other
}))
),
cmd("getpeers")
.hidden()
.action((_, conf) => conf.copy(command = GetPeers))
.text(s"List the connected peers"),
help('h', "help").text("Display this help message and exit"),
arg[String]("<cmd>")
.optional()
.text(
"The command and arguments to be executed. Try bitcoin-s-cli help for a list of all commands"),
checkConfig {
case Config(NoCommand, _, _) =>
failure("You need to provide a command!")
case _ => success
}
)
}
// TODO make this dynamic
val port = 9999
val host = "localhost"
val config: Config = OParser.parse(parser, args, Config()) match {
case None => sys.exit(1)
case Some(conf) => conf
}
import System.err.{println => printerr}
/** Prints the given message to stderr if debug is set */
def debug(message: Any): Unit = {
if (config.debug) {
printerr(s"DEBUG: $message")
}
}
/** Prints the given message to stderr and exist */
def error(message: String): Nothing = {
printerr(message)
// TODO error codes?
sys.exit(1)
}
case class RequestParam(
method: String,
params: Seq[ujson.Value.Value] = Nil) {
lazy val toJsonMap: Map[String, ujson.Value] = {
Map("method" -> method, "params" -> params)
}
}
val requestParam: RequestParam = config.command match {
case GetBalance =>
RequestParam("getbalance")
case GetNewAddress =>
RequestParam("getnewaddress")
case SendToAddress(address, bitcoins) =>
RequestParam("sendtoaddress",
Seq(up.writeJs(address), up.writeJs(bitcoins)))
// height
case GetBlockCount => RequestParam("getblockcount")
// besthash
case GetBestBlockHash => RequestParam("getbestblockhash")
// peers
case GetPeers => RequestParam("getpeers")
case NoCommand => ???
}
try {
import com.softwaremill.sttp._
implicit val backend = HttpURLConnectionBackend()
val request =
sttp
.post(uri"http://$host:$port/")
.contentType("application/json")
.body({
val uuid = ju.UUID.randomUUID.toString
val paramsWithID: Map[String, ujson.Value] = requestParam.toJsonMap + ("id" -> up
.writeJs(uuid))
up.write(paramsWithID)
})
debug(s"HTTP request: $request")
val response = request.send()
debug(s"HTTP response:")
debug(response)
// in order to mimic Bitcoin Core we always send
// an object looking like {"result": ..., "error": ...}
val rawBody = response.body match {
case Left(err) => err
case Right(response) => response
}
val js = ujson.read(rawBody)
val jsObj = try { js.obj } catch {
case _: Throwable =>
error(s"Response was not a JSON object! Got: $rawBody")
}
/** Gets the given key from jsObj if it exists
* and is not null */
def getKey(key: String): Option[ujson.Value] =
jsObj
.get(key)
.flatMap(result => if (result.isNull) None else Some(result))
/** Converts a `ujson.Value` to String, making an
* effort to avoid preceding and trailing `"`s */
def jsValueToString(value: ujson.Value) = value match {
case Str(string) => string
case Num(num) if num.isWhole => num.toLong.toString()
case Num(num) => num.toString()
case rest: ujson.Value => rest.toString()
}
(getKey("result"), getKey("error")) match {
case (Some(result), None) =>
val msg = jsValueToString(result)
println(msg)
case (None, Some(err)) =>
val msg = jsValueToString(err)
error(msg)
case (None, None) | (Some(_), Some(_)) =>
error(s"Got unexpected response: $rawBody")
}
} catch {
case _: ConnectException =>
error(
"Connection refused! Check that the server is running and configured correctly.")
}
}

View File

@ -0,0 +1,44 @@
package org.bitcoins.cli
import scopt._
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.protocol._
import org.bitcoins.core.currency._
import org.bitcoins.core.config.Networks
import scala.util.Failure
import scala.util.Success
/** scopt readers for parsing CLI params and options */
object CliReaders {
implicit val npReads: Read[NetworkParameters] =
new Read[NetworkParameters] {
val arity: Int = 1
val reads: String => NetworkParameters = str =>
Networks.knownNetworks
.find(_.toString.toLowerCase == str.toLowerCase)
.getOrElse {
val networks =
Networks.knownNetworks
.map(_.toString.toLowerCase)
.mkString(", ")
val msg =
s"$str is not a valid network! Valid networks: $networks"
sys.error(msg)
}
}
implicit val bitcoinAddressReads: Read[BitcoinAddress] =
new Read[BitcoinAddress] {
val arity: Int = 1
val reads: String => BitcoinAddress = BitcoinAddress.fromStringExn
}
implicit val bitcoinsReads: Read[Bitcoins] =
new Read[Bitcoins] {
val arity: Int = 1
val reads: String => Bitcoins = str => Bitcoins(BigDecimal(str))
}
}

View File

@ -0,0 +1,20 @@
package org.bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import upickle.default._
package object picklers {
import org.bitcoins.core.crypto.DoubleSha256DigestBE
implicit val bitcoinAddressPickler: ReadWriter[BitcoinAddress] =
readwriter[String]
.bimap(_.value, BitcoinAddress.fromStringExn(_))
implicit val bitcoinsPickler: ReadWriter[Bitcoins] =
readwriter[Double].bimap(_.toBigDecimal.toDouble, Bitcoins(_))
implicit val doubleSha256DigestBEPickler: ReadWriter[DoubleSha256DigestBE] =
readwriter[String].bimap(_.hex, DoubleSha256DigestBE.fromHex)
}

View File

@ -0,0 +1,3 @@
name := "bitcoin-s-server-test"
publish / skip := true

9
app/server/server.sbt Normal file
View File

@ -0,0 +1,9 @@
name := "bitcoin-s-server"
// Ensure actor system is shut down
// when server is quit
Compile / fork := true
publish / skip := true
libraryDependencies ++= Deps.server

View File

@ -0,0 +1,4 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
}

View File

@ -1,4 +1,4 @@
package org.bitcoins.testkit package org.bitcoins.server
import com.typesafe.config.Config import com.typesafe.config.Config
import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.config.WalletAppConfig
@ -6,8 +6,6 @@ import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.chain.config.ChainAppConfig import org.bitcoins.chain.config.ChainAppConfig
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
import scala.concurrent.Future import scala.concurrent.Future
import java.nio.file.Files
import com.typesafe.config.ConfigFactory
/** /**
* A unified config class for all submodules of Bitcoin-S * A unified config class for all submodules of Bitcoin-S
@ -31,7 +29,7 @@ case class BitcoinSAppConfig(private val confs: Config*) {
} }
/** The underlying config the result of our fields derive from */ /** The underlying config the result of our fields derive from */
lazy val config = { lazy val config: Config = {
assert(chainConf.config == nodeConf.config) assert(chainConf.config == nodeConf.config)
assert(nodeConf.config == walletConf.config) assert(nodeConf.config == walletConf.config)
@ -75,61 +73,4 @@ object BitcoinSAppConfig {
implicit def toNodeConf(conf: BitcoinSAppConfig): NodeAppConfig = implicit def toNodeConf(conf: BitcoinSAppConfig): NodeAppConfig =
conf.nodeConf conf.nodeConf
/**
* App configuration suitable for test purposes:
*
* 1) Data directory is set to user temp directory
*/
def getTestConfig(config: Config*) = {
val tmpDir = Files.createTempDirectory("bitcoin-s-")
val confStr = s"""
| bitcoin-s {
| datadir = $tmpDir
| }
|
|""".stripMargin
val conf = ConfigFactory.parseString(confStr)
val allConfs = conf +: config
BitcoinSAppConfig(allConfs: _*)
}
sealed trait ProjectType
object ProjectType {
case object Wallet extends ProjectType
case object Node extends ProjectType
case object Chain extends ProjectType
val all = List(Wallet, Node, Chain)
}
/** Generates a Typesafe config with DBs set to memory
* databases for the given project (or all, if no
* project is given). This configuration can then be
* given as a override to other configs.
*/
def configWithMemoryDb(project: Option[ProjectType]): Config = {
def memConfigForProject(project: ProjectType): String = {
val name = project.toString().toLowerCase()
s"""
| $name.db {
| url = "jdbc:sqlite:file:$name.db:?mode=memory&cache=shared"
| connectionPool = disabled
| keepAliveConnection = true
| }
|""".stripMargin
}
val confStr = project match {
case None => ProjectType.all.map(memConfigForProject).mkString("\n")
case Some(p) => memConfigForProject(p)
}
val nestedConfStr = s"""
| bitcoin-s {
| $confStr
| }
|""".stripMargin
ConfigFactory.parseString(nestedConfStr)
}
} }

View File

@ -0,0 +1,33 @@
package org.bitcoins.server
import akka.actor.ActorSystem
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.chain.api.ChainApi
import org.bitcoins.picklers._
case class ChainRoutes(chain: ChainApi)(implicit system: ActorSystem)
extends BitcoinSLogger
with ServerRoute {
implicit val materializer = ActorMaterializer()
import system.dispatcher
def handleCommand: PartialFunction[ServerCommand, StandardRoute] = {
case ServerCommand("getblockcount", _) =>
complete {
chain.getBlockCount.map { count =>
Server.httpSuccess(count)
}
}
case ServerCommand("getbestblockhash", _) =>
complete {
chain.getBestBlockHash.map { hash =>
Server.httpSuccess(hash)
}
}
}
}

View File

@ -0,0 +1,13 @@
package org.bitcoins.server
/** HTTP errors our server knows how to handle.
* These gets picked up by the exceptions handler
* in Main
*/
sealed abstract class HttpError extends Error
object HttpError {
/** The RPC method was not found */
final case class MethodNotFound(method: String) extends HttpError
}

View File

@ -0,0 +1,134 @@
package org.bitcoins.server
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.node.models.Peer
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import akka.actor.ActorSystem
import scala.concurrent.Await
import scala.concurrent.duration._
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.node.config.NodeAppConfig
import java.nio.file.Files
import scala.concurrent.Future
import org.bitcoins.wallet.LockedWallet
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.api.InitializeWalletSuccess
import org.bitcoins.wallet.api.InitializeWalletError
import org.bitcoins.node.SpvNode
import org.bitcoins.chain.blockchain.ChainHandler
import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.bitcoins.wallet.api.UnlockWalletSuccess
import org.bitcoins.wallet.api.UnlockWalletError
import org.bitcoins.node.networking.peer.DataMessageHandler
import org.bitcoins.node.SpvNodeCallbacks
import org.bitcoins.wallet.WalletStorage
object Main
extends App
// TODO we want to log to user data directory
// how do we do this?
with BitcoinSLogger {
implicit val conf = {
// val custom = ConfigFactory.parseString("bitcoin-s.network = testnet3")
BitcoinSAppConfig()
}
implicit val walletConf: WalletAppConfig = conf.walletConf
implicit val nodeConf: NodeAppConfig = conf.nodeConf
implicit val chainConf: ChainAppConfig = conf.chainConf
implicit val system = ActorSystem("bitcoin-s")
import system.dispatcher
sys.addShutdownHook {
logger.error(s"Exiting process")
system.terminate().foreach(_ => logger.info(s"Actor system terminated"))
}
/** Log the given message, shut down the actor system and quit. */
def error(message: Any): Nothing = {
logger.error(s"FATAL: $message")
logger.error(s"Shutting down actor system")
Await.result(system.terminate(), 10.seconds)
logger.error("Actor system terminated")
logger.error(s"Exiting")
sys.error(message.toString())
}
/** Checks if the user already has a wallet */
def hasWallet(): Boolean = {
val walletDB = walletConf.dbPath resolve walletConf.dbName
Files.exists(walletDB) && WalletStorage.seedExists()
}
val walletInitF: Future[UnlockedWalletApi] = if (hasWallet()) {
logger.info(s"Using pre-existing wallet")
val locked = LockedWallet()
// TODO change me when we implement proper password handling
locked.unlock(Wallet.badPassphrase) match {
case UnlockWalletSuccess(wallet) => Future.successful(wallet)
case err: UnlockWalletError => error(err)
}
} else {
logger.info(s"Creating new wallet")
Wallet.initialize().map {
case InitializeWalletSuccess(wallet) => wallet
case err: InitializeWalletError => error(err)
}
}
val bitcoind = BitcoindInstance.fromDatadir()
val bitcoindCli = new BitcoindRpcClient(bitcoind)
val peer = Peer.fromBitcoind(bitcoind)
val startFut = for {
_ <- bitcoindCli.isStartedF.map { started =>
if (!started) error("Local bitcoind is not started!")
}
_ <- bitcoindCli.getBlockChainInfo.map { bitcoindInfo =>
if (bitcoindInfo.chain != nodeConf.network)
error(
s"bitcoind and Bitcoin-S node are on different chains! Bitcoind: ${bitcoindInfo.chain}. Bitcoin-S node: ${nodeConf.network}")
}
_ <- conf.initialize()
wallet <- walletInitF
bloom <- wallet.getBloomFilter()
_ = logger.info(s"Got bloom filter with ${bloom.filterSize.toInt} elements")
node <- {
val callbacks = {
import DataMessageHandler._
val onTX: OnTxReceived = { tx =>
wallet.processTransaction(tx, confirmations = 0)
()
}
SpvNodeCallbacks(onTxReceived = Seq(onTX))
}
val blockheaderDAO = BlockHeaderDAO()
val chain = ChainHandler(blockheaderDAO, conf)
SpvNode(peer, chain, bloom, callbacks).start()
}
_ = logger.info(s"Starting SPV node sync")
_ <- node.sync()
start <- {
val walletRoutes = WalletRoutes(wallet)
val nodeRoutes = NodeRoutes(node)
val chainRoutes = ChainRoutes(node.chainApi)
val server = Server(Seq(walletRoutes, nodeRoutes, chainRoutes))
server.start()
}
} yield start
startFut.failed.foreach { err =>
logger.info(s"Error on server startup!", err)
}
}

View File

@ -0,0 +1,21 @@
package org.bitcoins.server
import akka.actor.ActorSystem
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.node.SpvNode
case class NodeRoutes(node: SpvNode)(implicit system: ActorSystem)
extends BitcoinSLogger
with ServerRoute {
implicit val materializer = ActorMaterializer()
def handleCommand: PartialFunction[ServerCommand, StandardRoute] = {
case ServerCommand("getpeers", _) =>
complete {
Server.httpSuccess("TODO implement getpeers")
}
}
}

View File

@ -0,0 +1,131 @@
package org.bitcoins.server
import upickle.{default => up}
import akka.actor.ActorSystem
import akka.http.scaladsl._
import akka.stream.ActorMaterializer
import org.bitcoins.core.util.BitcoinSLogger
import akka.http.scaladsl.model._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import de.heikoseeberger.akkahttpupickle.UpickleSupport._
import akka.http.scaladsl.server.directives.DebuggingDirectives
import akka.event.Logging
case class Server(handlers: Seq[ServerRoute])(implicit system: ActorSystem)
extends BitcoinSLogger {
implicit val materializer = ActorMaterializer()
import system.dispatcher
/** Handles all server commands by throwing a MethodNotFound */
private val catchAllHandler: PartialFunction[ServerCommand, StandardRoute] = {
case ServerCommand(name, _) => throw HttpError.MethodNotFound(name)
}
/** HTTP directive that handles both exceptions and rejections */
private def withErrorHandling(route: Route): Route = {
val rejectionHandler =
RejectionHandler
.newBuilder()
.handleNotFound {
complete {
Server.httpError(
"""Resource not found. Hint: all RPC calls are made against root ('/')""",
StatusCodes.BadRequest)
}
}
.result()
val exceptionHandler = ExceptionHandler {
case HttpError.MethodNotFound(method) =>
complete(
Server.httpError(s"'$method' is not a valid method",
StatusCodes.BadRequest))
case err: Throwable =>
logger.info(s"Unhandled error in server:", err)
complete(Server.httpError("There was an error"))
}
handleRejections(rejectionHandler) {
handleExceptions(exceptionHandler) {
route
}
}
}
val route =
// TODO implement better logging
DebuggingDirectives.logRequestResult("http-rpc-server", Logging.InfoLevel) {
withErrorHandling {
pathSingleSlash {
post {
entity(as[ServerCommand]) { cmd =>
val init = PartialFunction.empty[ServerCommand, StandardRoute]
val handler = handlers.foldLeft(init) {
case (accum, curr) => accum.orElse(curr.handleCommand)
}
handler.orElse(catchAllHandler).apply(cmd)
}
}
}
}
}
def start() = {
val httpFut =
Http().bindAndHandle(route, "localhost", 9999)
httpFut.foreach { http =>
logger.info(s"Started Bitcoin-S HTTP server at ${http.localAddress}")
}
httpFut
}
}
object Server extends BitcoinSLogger {
// TODO id parameter
case class Response(
result: Option[ujson.Value] = None,
error: Option[String] = None) {
def toJsonMap: Map[String, ujson.Value] = {
Map(
"result" -> (result match {
case None => ujson.Null
case Some(res) => res
}),
"error" -> (error match {
case None => ujson.Null
case Some(err) => err
})
)
}
}
/** Creates a HTTP response with the given body as a JSON response */
def httpSuccess[T](body: T)(
implicit writer: up.Writer[T]): HttpEntity.Strict = {
val response = Response(result = Some(up.writeJs(body)))
HttpEntity(
ContentTypes.`application/json`,
up.write(response.toJsonMap)
)
}
def httpError(
msg: String,
status: StatusCode = StatusCodes.InternalServerError): HttpResponse = {
val entity = {
val response = Response(error = Some(msg))
HttpEntity(
ContentTypes.`application/json`,
up.write(response.toJsonMap)
)
}
HttpResponse(status = status, entity = entity)
}
}

View File

@ -0,0 +1,47 @@
package org.bitcoins.server
import upickle.default._
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.picklers._
import scala.util.Failure
import scala.util.Try
import scala.util.Success
import akka.io.Udp.Send
// TODO ID?
case class ServerCommand(method: String, params: ujson.Arr)
object ServerCommand {
implicit val rw: ReadWriter[ServerCommand] = macroRW
}
case class SendToAddress(address: BitcoinAddress, amount: Bitcoins)
object SendToAddress {
/// TODO do this in a more coherent fashion
// custom akka-http directive?
def fromJsArr(jsArr: ujson.Arr): Try[SendToAddress] = {
jsArr.arr.toList match {
case addrJs :: bitcoinsJs :: Nil =>
try {
val address = BitcoinAddress.fromStringExn(addrJs.str)
val bitcoins = Bitcoins(bitcoinsJs.num)
Success(SendToAddress(address, bitcoins))
} catch {
case e: Throwable => Failure(e)
}
case Nil =>
Failure(
new IllegalArgumentException("Missing address and amount argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 2"))
}
}
}

View File

@ -0,0 +1,6 @@
package org.bitcoins.server
import akka.http.scaladsl.server.StandardRoute
trait ServerRoute {
def handleCommand: PartialFunction[ServerCommand, StandardRoute]
}

View File

@ -0,0 +1,60 @@
package org.bitcoins.server
import org.bitcoins.picklers._
import akka.actor.ActorSystem
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.core.currency._
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import de.heikoseeberger.akkahttpupickle.UpickleSupport._
import scala.util.Failure
import scala.util.Success
case class WalletRoutes(wallet: UnlockedWalletApi)(implicit system: ActorSystem)
extends BitcoinSLogger
with ServerRoute {
import system.dispatcher
implicit val materializer = ActorMaterializer()
def handleCommand: PartialFunction[ServerCommand, StandardRoute] = {
case ServerCommand("getbalance", _) =>
complete {
wallet.getBalance().map { balance =>
Server.httpSuccess(
Bitcoins(balance.satoshis)
)
}
}
case ServerCommand("getnewaddress", _) =>
complete {
wallet.getNewAddress().map { address =>
Server.httpSuccess(address)
}
}
case ServerCommand("sendtoaddress", arr) =>
// TODO create custom directive for this?
SendToAddress.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(SendToAddress(address, bitcoins)) =>
complete {
// TODO dynamic fees
val feeRate = SatoshisPerByte(100.sats)
wallet.sendToAddress(address, bitcoins, feeRate).map { tx =>
// TODO this TX isn't being broadcast anywhere
// would be better to dump the entire TX hex until that's implemented?
Server.httpSuccess(tx.txIdBE)
}
}
}
}
}

View File

@ -34,4 +34,7 @@ buildInfoPackage := "org.bitcoins.docs"
// Mdoc end // Mdoc end
/////// ///////
Test / bloopGenerate := None
Compile / bloopGenerate := None
libraryDependencies ++= Deps.docs libraryDependencies ++= Deps.docs

View File

@ -9,6 +9,7 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future import scala.concurrent.Future
import org.bitcoins.core.config.RegTest
class BlockchainRpcTest extends BitcoindRpcTest { class BlockchainRpcTest extends BitcoindRpcTest {
@ -75,7 +76,7 @@ class BlockchainRpcTest extends BitcoindRpcTest {
info <- client.getBlockChainInfo info <- client.getBlockChainInfo
bestHash <- client.getBestBlockHash bestHash <- client.getBestBlockHash
} yield { } yield {
assert(info.chain == "regtest") assert(info.chain == RegTest)
assert(info.softforks.length >= 3) assert(info.softforks.length >= 3)
assert(info.bip9_softforks.keySet.size >= 2) assert(info.bip9_softforks.keySet.size >= 2)
assert(info.bestblockhash == bestHash) assert(info.bestblockhash == bestHash)

View File

@ -5,6 +5,7 @@ import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.number.{Int32, UInt32}
import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.wallet.fee.BitcoinFeeUnit import org.bitcoins.core.wallet.fee.BitcoinFeeUnit
import org.bitcoins.core.config.NetworkParameters
sealed abstract class BlockchainResult sealed abstract class BlockchainResult
@ -51,7 +52,7 @@ case class GetBlockWithTransactionsResult(
extends BlockchainResult extends BlockchainResult
case class GetBlockChainInfoResult( case class GetBlockChainInfoResult(
chain: String, chain: NetworkParameters,
blocks: Int, blocks: Int,
headers: Int, headers: Int,
bestblockhash: DoubleSha256DigestBE, bestblockhash: DoubleSha256DigestBE,
@ -106,6 +107,7 @@ case class GetBlockHeaderResult(
previousblockhash: Option[DoubleSha256DigestBE], previousblockhash: Option[DoubleSha256DigestBE],
nextblockhash: Option[DoubleSha256DigestBE]) nextblockhash: Option[DoubleSha256DigestBE])
extends BlockchainResult { extends BlockchainResult {
def blockHeader: BlockHeader = { def blockHeader: BlockHeader = {
//prevblockhash is only empty if we have the genesis block //prevblockhash is only empty if we have the genesis block

View File

@ -77,18 +77,6 @@ lazy val commonSettings = List(
assemblyOption in assembly := (assemblyOption in assembly).value assemblyOption in assembly := (assemblyOption in assembly).value
.copy(includeScala = false), .copy(includeScala = false),
licenses += ("MIT", url("http://opensource.org/licenses/MIT")), licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
/**
* Adding Ammonite REPL to test scope, can access both test and compile
* sources. Docs: http://ammonite.io/#Ammonite-REPL
* Creates an ad-hoc main file that can be run by doing
* test:run (or test:runMain amm if there's multiple main files
* in scope)
*/
Test / sourceGenerators += Def.task {
val file = (Test / sourceManaged).value / "amm.scala"
IO.write(file, """object amm extends App { ammonite.Main.main(args) }""")
Seq(file)
}.taskValue,
// Travis has performance issues on macOS // Travis has performance issues on macOS
Test / parallelExecution := !(Properties.isMac && sys.props Test / parallelExecution := !(Properties.isMac && sys.props
.get("CI") .get("CI")
@ -133,6 +121,8 @@ lazy val bitcoins = project
secp256k1jni, secp256k1jni,
chain, chain,
chainTest, chainTest,
cli,
cliTest,
core, core,
coreTest, coreTest,
dbCommons, dbCommons,
@ -143,8 +133,11 @@ lazy val bitcoins = project
eclairRpcTest, eclairRpcTest,
node, node,
nodeTest, nodeTest,
picklers,
wallet, wallet,
walletTest, walletTest,
walletServer,
walletServerTest,
testkit, testkit,
scripts, scripts,
zmq zmq
@ -152,7 +145,6 @@ lazy val bitcoins = project
.settings(commonSettings: _*) .settings(commonSettings: _*)
// crossScalaVersions must be set to Nil on the aggregating project // crossScalaVersions must be set to Nil on the aggregating project
.settings(crossScalaVersions := Nil) .settings(crossScalaVersions := Nil)
.settings(libraryDependencies ++= Deps.root)
.enablePlugins(ScalaUnidocPlugin, GitVersioning) .enablePlugins(ScalaUnidocPlugin, GitVersioning)
.settings( .settings(
// we modify the unidoc task to move the generated Scaladocs into the // we modify the unidoc task to move the generated Scaladocs into the
@ -292,6 +284,48 @@ lazy val coreTest = project
) )
.enablePlugins() .enablePlugins()
lazy val walletServer = project
.in(file("app/server"))
.settings(commonSettings: _*)
.dependsOn(
picklers,
node,
chain,
wallet,
bitcoindRpc
)
lazy val walletServerTest = project
.in(file("app/server-test"))
.settings(commonTestSettings)
.dependsOn(
walletServer,
testkit
)
// internal picklers used by server
// and CLI
lazy val picklers = project
.in(file("app/picklers"))
.dependsOn(core % testAndCompile)
lazy val cli = project
.in(file("app/cli"))
.settings(commonSettings: _*)
.dependsOn(
picklers
)
lazy val cliTest = project
.in(file("app/cli-test"))
.settings(commonTestSettings: _*)
.dependsOn(
cli,
testkit
)
lazy val chainDbSettings = dbFlywaySettings("chaindb") lazy val chainDbSettings = dbFlywaySettings("chaindb")
lazy val chain = project lazy val chain = project
.in(file("chain")) .in(file("chain"))
@ -428,6 +462,8 @@ lazy val testkit = project
.settings(commonSettings: _*) .settings(commonSettings: _*)
.dependsOn( .dependsOn(
core % testAndCompile, core % testAndCompile,
walletServer,
cli,
chain, chain,
bitcoindRpc, bitcoindRpc,
eclairRpc, eclairRpc,
@ -492,19 +528,6 @@ lazy val scripts = project
zmq zmq
) )
// Ammonite is invoked through running
// a main class it places in test sources
// for us. This makes it a bit less awkward
// to start the Ammonite shell. Sadly,
// prepending the project and then doing
// `amm` (e.g. sbt coreTest/amm`) does not
// work. For that you either have to do
// `sbt coreTest/test:run` or:
// sbt
// project coreTest
// amm
addCommandAlias("amm", "test:run")
publishArtifact in bitcoins := false publishArtifact in bitcoins := false
def dbFlywaySettings(dbName: String): List[Setting[_]] = { def dbFlywaySettings(dbName: String): List[Setting[_]] = {

View File

@ -20,7 +20,8 @@ import org.scalatest.{Assertion, FutureOutcome}
import play.api.libs.json.Json import play.api.libs.json.Json
import scala.concurrent.Future import scala.concurrent.Future
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
class ChainHandlerTest extends ChainUnitTest { class ChainHandlerTest extends ChainUnitTest {
@ -30,8 +31,10 @@ class ChainHandlerTest extends ChainUnitTest {
// we're working with mainnet data // we're working with mainnet data
implicit override lazy val appConfig: ChainAppConfig = { implicit override lazy val appConfig: ChainAppConfig = {
val memoryDb = BitcoinSAppConfig.configWithMemoryDb( import BitcoinSTestAppConfig.ProjectType
Some(BitcoinSAppConfig.ProjectType.Chain))
val memoryDb =
BitcoinSTestAppConfig.configWithMemoryDb(Some(ProjectType.Chain))
mainnetAppConfig.withOverrides(memoryDb) mainnetAppConfig.withOverrides(memoryDb)
} }

View File

@ -9,15 +9,17 @@ import org.bitcoins.testkit.chain.{ChainTestUtil, ChainUnitTest}
import org.scalatest.FutureOutcome import org.scalatest.FutureOutcome
import scala.concurrent.Future import scala.concurrent.Future
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
class BitcoinPowTest extends ChainUnitTest { class BitcoinPowTest extends ChainUnitTest {
override type FixtureParam = ChainFixture override type FixtureParam = ChainFixture
implicit override lazy val appConfig: ChainAppConfig = { implicit override lazy val appConfig: ChainAppConfig = {
val memoryDb = BitcoinSAppConfig.configWithMemoryDb( import BitcoinSTestAppConfig.ProjectType
Some(BitcoinSAppConfig.ProjectType.Chain)) val memoryDb =
BitcoinSTestAppConfig.configWithMemoryDb(Some(ProjectType.Chain))
mainnetAppConfig.withOverrides(memoryDb) mainnetAppConfig.withOverrides(memoryDb)
} }

View File

@ -18,7 +18,7 @@ import org.scalatest.{Assertion, FutureOutcome}
import scala.concurrent.Future import scala.concurrent.Future
import org.bitcoins.chain.config.ChainAppConfig import org.bitcoins.chain.config.ChainAppConfig
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
class TipValidationTest extends ChainUnitTest { class TipValidationTest extends ChainUnitTest {

View File

@ -1,5 +1,8 @@
scalaVersion in ThisBuild := "2.12.8" val scala2_11 = "2.11.12"
val scala2_12 = "2.12.8"
crossScalaVersions in ThisBuild := List("2.11.12", "2.12.8") scalaVersion in ThisBuild := scala2_12
crossScalaVersions in ThisBuild := List(scala2_11, scala2_12)
organization in ThisBuild := "org.bitcoins" organization in ThisBuild := "org.bitcoins"

View File

@ -7,7 +7,7 @@ import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.node.config.NodeAppConfig import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.models.Peer import org.bitcoins.node.models.Peer
import org.scalatest.FutureOutcome import org.scalatest.FutureOutcome
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.bitcoins.testkit.wallet.BitcoinSWalletTest

View File

@ -5,7 +5,8 @@ import akka.testkit.{TestActorRef, TestProbe}
import org.bitcoins.node.models.Peer import org.bitcoins.node.models.Peer
import org.bitcoins.node.networking.peer.PeerMessageReceiver import org.bitcoins.node.networking.peer.PeerMessageReceiver
import org.bitcoins.node.networking.peer.PeerMessageReceiverState.Preconnection import org.bitcoins.node.networking.peer.PeerMessageReceiverState.Preconnection
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.async.TestAsyncUtil import org.bitcoins.testkit.async.TestAsyncUtil
import org.bitcoins.testkit.node.NodeTestUtil import org.bitcoins.testkit.node.NodeTestUtil
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
@ -25,19 +26,21 @@ class ClientTest
with BeforeAndAfterAll { with BeforeAndAfterAll {
implicit private val config: BitcoinSAppConfig = implicit private val config: BitcoinSAppConfig =
BitcoinSAppConfig.getTestConfig() BitcoinSTestAppConfig.getTestConfig()
implicit private val chainConf = config.chainConf implicit private val chainConf = config.chainConf
implicit private val nodeConf = config.nodeConf implicit private val nodeConf = config.nodeConf
implicit val np = config.chainConf.network implicit val np = config.chainConf.network
lazy val bitcoindRpcF = BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum) lazy val bitcoindRpcF =
BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum)
lazy val bitcoindPeerF = bitcoindRpcF.map { bitcoind => lazy val bitcoindPeerF = bitcoindRpcF.map { bitcoind =>
NodeTestUtil.getBitcoindPeer(bitcoind) NodeTestUtil.getBitcoindPeer(bitcoind)
} }
lazy val bitcoindRpc2F = BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum) lazy val bitcoindRpc2F =
BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum)
lazy val bitcoindPeer2F = bitcoindRpcF.map { bitcoind => lazy val bitcoindPeer2F = bitcoindRpcF.map { bitcoind =>
NodeTestUtil.getBitcoindPeer(bitcoind) NodeTestUtil.getBitcoindPeer(bitcoind)

View File

@ -25,8 +25,16 @@ object Deps {
val akkaActorV = akkaStreamv val akkaActorV = akkaStreamv
val slickV = "3.3.1" val slickV = "3.3.1"
val sqliteV = "3.27.2.1" val sqliteV = "3.27.2.1"
val uJsonV = "0.7.1"
val scalameterV = "0.17" val scalameterV = "0.17"
// Wallet/node/chain server deps
val uPickleV = "0.7.4"
val akkaHttpUpickleV = "1.27.0"
val uJsonV = uPickleV // Li Haoyi ecosystem does common versioning
// CLI deps
val scoptV = "4.0.0-RC2"
val sttpV = "1.6.0"
} }
object Compile { object Compile {
@ -37,6 +45,8 @@ object Deps {
val akkaHttp = "com.typesafe.akka" %% "akka-http" % V.akkav withSources () withJavadoc () val akkaHttp = "com.typesafe.akka" %% "akka-http" % V.akkav withSources () withJavadoc ()
val akkaStream = "com.typesafe.akka" %% "akka-stream" % V.akkaStreamv withSources () withJavadoc () val akkaStream = "com.typesafe.akka" %% "akka-stream" % V.akkaStreamv withSources () withJavadoc ()
val akkaActor = "com.typesafe.akka" %% "akka-actor" % V.akkaStreamv withSources () withJavadoc () val akkaActor = "com.typesafe.akka" %% "akka-actor" % V.akkaStreamv withSources () withJavadoc ()
val akkaLog = "com.typesafe.akka" %% "akka-slf4j" % V.akkaStreamv
val playJson = "com.typesafe.play" %% "play-json" % V.playv withSources () withJavadoc () val playJson = "com.typesafe.play" %% "play-json" % V.playv withSources () withJavadoc ()
val typesafeConfig = "com.typesafe" % "config" % V.typesafeConfigV withSources () withJavadoc () val typesafeConfig = "com.typesafe" % "config" % V.typesafeConfigV withSources () withJavadoc ()
@ -55,6 +65,18 @@ object Deps {
val postgres = "org.postgresql" % "postgresql" % V.postgresV val postgres = "org.postgresql" % "postgresql" % V.postgresV
val uJson = "com.lihaoyi" %% "ujson" % V.uJsonV val uJson = "com.lihaoyi" %% "ujson" % V.uJsonV
// serializing to and from JSON
val uPickle = "com.lihaoyi" %% "upickle" % V.uPickleV
// make akka-http play nice with upickle
val akkaHttpUpickle = "de.heikoseeberger" %% "akka-http-upickle" % V.akkaHttpUpickleV
// parsing of CLI opts and args
val scopt = "com.github.scopt" %% "scopt" % V.scoptV
// HTTP client lib
val sttp = "com.softwaremill.sttp" %% "core" % V.sttpV
val scalacheck = "org.scalacheck" %% "scalacheck" % V.scalacheck withSources () withJavadoc () val scalacheck = "org.scalacheck" %% "scalacheck" % V.scalacheck withSources () withJavadoc ()
val scalaTest = "org.scalatest" %% "scalatest" % V.scalaTest withSources () withJavadoc () val scalaTest = "org.scalatest" %% "scalatest" % V.scalaTest withSources () withJavadoc ()
} }
@ -71,37 +93,28 @@ object Deps {
val spray = "io.spray" %% "spray-json" % V.spray % "test" withSources () withJavadoc () val spray = "io.spray" %% "spray-json" % V.spray % "test" withSources () withJavadoc ()
val akkaHttp = "com.typesafe.akka" %% "akka-http-testkit" % V.akkav % "test" withSources () withJavadoc () val akkaHttp = "com.typesafe.akka" %% "akka-http-testkit" % V.akkav % "test" withSources () withJavadoc ()
val akkaStream = "com.typesafe.akka" %% "akka-stream-testkit" % V.akkaStreamv % "test" withSources () withJavadoc () val akkaStream = "com.typesafe.akka" %% "akka-stream-testkit" % V.akkaStreamv % "test" withSources () withJavadoc ()
val ammonite = Compile.ammonite % "test"
val playJson = Compile.playJson % "test" val playJson = Compile.playJson % "test"
val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % V.akkaActorV withSources () withJavadoc () val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % V.akkaActorV withSources () withJavadoc ()
val scalameter = "com.storm-enroute" %% "scalameter" % V.scalameterV % "test" withSources () withJavadoc () val scalameter = "com.storm-enroute" %% "scalameter" % V.scalameterV % "test" withSources () withJavadoc ()
} }
val root = List(
Test.ammonite
)
val chain = List( val chain = List(
Compile.slf4j, Compile.slf4j
Test.ammonite
) )
val chainTest = List( val chainTest = List(
Test.ammonite,
Test.logback Test.logback
) )
val core = List( val core = List(
Compile.bouncycastle, Compile.bouncycastle,
Compile.scodec, Compile.scodec,
Compile.slf4j, Compile.slf4j
Test.ammonite
) )
val secp256k1jni = List( val secp256k1jni = List(
Compile.nativeLoader, Compile.nativeLoader,
Test.junitInterface, Test.junitInterface
Test.ammonite
) )
val coreTest = List( val coreTest = List(
@ -110,7 +123,6 @@ object Deps {
Test.logback, Test.logback,
Test.scalaTest, Test.scalaTest,
Test.spray, Test.spray,
Test.ammonite,
Test.playJson Test.playJson
) )
@ -119,8 +131,7 @@ object Deps {
Compile.slf4j, Compile.slf4j,
Test.logback, Test.logback,
Test.scalacheck, Test.scalacheck,
Test.scalaTest, Test.scalaTest
Test.ammonite
) )
val bitcoindRpc = List( val bitcoindRpc = List(
@ -128,8 +139,7 @@ object Deps {
Compile.akkaStream, Compile.akkaStream,
Compile.playJson, Compile.playJson,
Compile.slf4j, Compile.slf4j,
Compile.typesafeConfig, Compile.typesafeConfig
Test.ammonite
) )
val bitcoindRpcTest = List( val bitcoindRpcTest = List(
@ -138,29 +148,43 @@ object Deps {
Test.logback, Test.logback,
Test.scalaTest, Test.scalaTest,
Test.scalacheck, Test.scalacheck,
Test.async, Test.async
Test.ammonite
) )
val bench = List( val bench = List(
"org.slf4j" % "slf4j-api" % V.slf4j withSources () withJavadoc (), "org.slf4j" % "slf4j-api" % V.slf4j withSources () withJavadoc (),
Compile.logback, Compile.logback
Test.ammonite
) )
val dbCommons = List( val dbCommons = List(
Compile.slick, Compile.slick,
Compile.sqlite, Compile.sqlite,
Compile.slickHikari, Compile.slickHikari
Test.ammonite )
val cli = List(
Compile.sttp,
Compile.uPickle,
Compile.scopt
)
val picklers = List(
Compile.uPickle
)
val server = List(
Compile.akkaHttpUpickle,
Compile.uPickle,
Compile.logback,
Compile.akkaLog,
Compile.akkaHttp
) )
val eclairRpc = List( val eclairRpc = List(
Compile.akkaHttp, Compile.akkaHttp,
Compile.akkaStream, Compile.akkaStream,
Compile.playJson, Compile.playJson,
Compile.slf4j, Compile.slf4j
Test.ammonite
) )
val eclairRpcTest = List( val eclairRpcTest = List(
@ -168,8 +192,7 @@ object Deps {
Test.akkaStream, Test.akkaStream,
Test.logback, Test.logback,
Test.scalaTest, Test.scalaTest,
Test.scalacheck, Test.scalacheck
Test.ammonite
) )
val node = List( val node = List(
@ -178,23 +201,20 @@ object Deps {
Compile.joda, Compile.joda,
Compile.slick, Compile.slick,
Compile.slickHikari, Compile.slickHikari,
Compile.sqlite, Compile.sqlite
Test.ammonite
) )
val nodeTest = List( val nodeTest = List(
Test.akkaTestkit, Test.akkaTestkit,
Test.logback, Test.logback,
Test.scalaTest, Test.scalaTest
Test.ammonite
) )
val testkit = List( val testkit = List(
Compile.slf4j, Compile.slf4j,
Compile.scalacheck, Compile.scalacheck,
Compile.scalaTest, Compile.scalaTest,
Test.akkaTestkit, Test.akkaTestkit
Test.ammonite
) )
val scripts = List( val scripts = List(
@ -203,18 +223,15 @@ object Deps {
) )
val wallet = List( val wallet = List(
Test.ammonite,
Compile.uJson Compile.uJson
) )
val walletTest = List( val walletTest = List(
Test.logback, Test.logback,
Test.akkaTestkit, Test.akkaTestkit
Test.ammonite
) )
val docs = List( val docs = List(
Compile.ammonite,
Compile.logback, Compile.logback,
Test.scalaTest, Test.scalaTest,
Test.logback Test.logback

View File

@ -0,0 +1,65 @@
package org.bitcoins.testkit
import org.bitcoins.server.BitcoinSAppConfig
import com.typesafe.config._
import java.nio.file._
object BitcoinSTestAppConfig {
/**
* App configuration suitable for test purposes:
*
* 1) Data directory is set to user temp directory
*/
def getTestConfig(config: Config*): BitcoinSAppConfig = {
val tmpDir = Files.createTempDirectory("bitcoin-s-")
val confStr = s"""
| bitcoin-s {
| datadir = $tmpDir
| }
|
|""".stripMargin
val conf = ConfigFactory.parseString(confStr)
val allConfs = conf +: config
BitcoinSAppConfig(allConfs: _*)
}
sealed trait ProjectType
object ProjectType {
case object Wallet extends ProjectType
case object Node extends ProjectType
case object Chain extends ProjectType
val all = List(Wallet, Node, Chain)
}
/** Generates a Typesafe config with DBs set to memory
* databases for the given project (or all, if no
* project is given). This configuration can then be
* given as a override to other configs.
*/
def configWithMemoryDb(project: Option[ProjectType]): Config = {
def memConfigForProject(project: ProjectType): String = {
val name = project.toString().toLowerCase()
s"""
| $name.db {
| url = "jdbc:sqlite:file:$name.db:?mode=memory&cache=shared"
| connectionPool = disabled
| keepAliveConnection = true
| }
|""".stripMargin
}
val confStr = project match {
case None => ProjectType.all.map(memConfigForProject).mkString("\n")
case Some(p) => memConfigForProject(p)
}
val nestedConfStr = s"""
| bitcoin-s {
| $confStr
| }
|""".stripMargin
ConfigFactory.parseString(nestedConfStr)
}
}

View File

@ -20,7 +20,6 @@ import org.bitcoins.testkit.chain.fixture._
import org.bitcoins.testkit.fixtures.BitcoinSFixture import org.bitcoins.testkit.fixtures.BitcoinSFixture
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.zmq.ZMQSubscriber import org.bitcoins.zmq.ZMQSubscriber
import org.bitcoins.testkit.BitcoinSAppConfig
import org.scalatest._ import org.scalatest._
import play.api.libs.json.{JsError, JsSuccess, Json} import play.api.libs.json.{JsError, JsSuccess, Json}
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -29,6 +28,7 @@ import scala.annotation.tailrec
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import org.bitcoins.db.AppConfig import org.bitcoins.db.AppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
trait ChainUnitTest trait ChainUnitTest
extends org.scalatest.fixture.AsyncFlatSpec extends org.scalatest.fixture.AsyncFlatSpec
@ -46,7 +46,7 @@ trait ChainUnitTest
implicit lazy val chainParam: ChainParams = appConfig.chain implicit lazy val chainParam: ChainParams = appConfig.chain
implicit lazy val appConfig: ChainAppConfig = implicit lazy val appConfig: ChainAppConfig =
BitcoinSAppConfig.getTestConfig() BitcoinSTestAppConfig.getTestConfig()
/** /**
* Behaves exactly like the default conf, execpt * Behaves exactly like the default conf, execpt
@ -54,7 +54,7 @@ trait ChainUnitTest
*/ */
lazy val mainnetAppConfig: ChainAppConfig = { lazy val mainnetAppConfig: ChainAppConfig = {
val mainnetConf = ConfigFactory.parseString("bitcoin-s.network = mainnet") val mainnetConf = ConfigFactory.parseString("bitcoin-s.network = mainnet")
BitcoinSAppConfig.getTestConfig(mainnetConf) BitcoinSTestAppConfig.getTestConfig(mainnetConf)
} }
override def beforeAll(): Unit = { override def beforeAll(): Unit = {

View File

@ -14,8 +14,8 @@ import org.bitcoins.node.networking.peer.{
PeerMessageSender PeerMessageSender
} }
import org.bitcoins.rpc.client.common.BitcoindRpcClient import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig._ import org.bitcoins.server.BitcoinSAppConfig._
import org.bitcoins.testkit.chain.ChainUnitTest import org.bitcoins.testkit.chain.ChainUnitTest
import org.bitcoins.testkit.fixtures.BitcoinSFixture import org.bitcoins.testkit.fixtures.BitcoinSFixture
import org.bitcoins.testkit.node.fixture.SpvNodeConnectedWithBitcoind import org.bitcoins.testkit.node.fixture.SpvNodeConnectedWithBitcoind
@ -29,6 +29,7 @@ import org.scalatest.{
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import org.bitcoins.testkit.BitcoinSTestAppConfig
trait NodeUnitTest trait NodeUnitTest
extends BitcoinSFixture extends BitcoinSFixture
@ -57,7 +58,7 @@ trait NodeUnitTest
/** Wallet config with data directory set to user temp directory */ /** Wallet config with data directory set to user temp directory */
implicit protected lazy val config: BitcoinSAppConfig = implicit protected lazy val config: BitcoinSAppConfig =
BitcoinSAppConfig.getTestConfig() BitcoinSTestAppConfig.getTestConfig()
implicit lazy val np: NetworkParameters = config.nodeConf.network implicit lazy val np: NetworkParameters = config.nodeConf.network

View File

@ -19,9 +19,10 @@ import org.scalatest._
import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import org.bitcoins.db.AppConfig import org.bitcoins.db.AppConfig
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import org.bitcoins.testkit.BitcoinSTestAppConfig
trait BitcoinSWalletTest trait BitcoinSWalletTest
extends fixture.AsyncFlatSpec extends fixture.AsyncFlatSpec
@ -35,7 +36,7 @@ trait BitcoinSWalletTest
/** Wallet config with data directory set to user temp directory */ /** Wallet config with data directory set to user temp directory */
implicit protected lazy val config: BitcoinSAppConfig = implicit protected lazy val config: BitcoinSAppConfig =
BitcoinSAppConfig.getTestConfig() BitcoinSTestAppConfig.getTestConfig()
/** Timeout for async operations */ /** Timeout for async operations */
protected val timeout: FiniteDuration = 10.seconds protected val timeout: FiniteDuration = 10.seconds

View File

@ -2,8 +2,8 @@ package org.bitcoins.testkit.db
import org.bitcoins.testkit.util.BitcoinSUnitTest import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.Implicits._ import org.bitcoins.testkit.Implicits._
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig._ import org.bitcoins.server.BitcoinSAppConfig._
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import org.bitcoins.core.config.TestNet3 import org.bitcoins.core.config.TestNet3
import org.bitcoins.chain.models.BlockHeaderDAO import org.bitcoins.chain.models.BlockHeaderDAO
@ -26,6 +26,7 @@ import org.bitcoins.db.SQLiteTableInfo
import slick.jdbc.SQLiteProfile.api._ import slick.jdbc.SQLiteProfile.api._
import org.bitcoins.db.CRUD import org.bitcoins.db.CRUD
import java.nio.file.Files import java.nio.file.Files
import org.bitcoins.testkit.BitcoinSTestAppConfig
class AppConfigTest extends BitcoinSUnitTest { class AppConfigTest extends BitcoinSUnitTest {
@ -38,7 +39,7 @@ class AppConfigTest extends BitcoinSUnitTest {
val networkOverride = val networkOverride =
ConfigFactory.parseString("bitcoin-s.network = testnet3") ConfigFactory.parseString("bitcoin-s.network = testnet3")
val config = BitcoinSAppConfig.getTestConfig(networkOverride) val config = BitcoinSTestAppConfig.getTestConfig(networkOverride)
val chainConf = config.chainConf val chainConf = config.chainConf
val walletConf = config.walletConf val walletConf = config.walletConf
val nodeConf = config.nodeConf val nodeConf = config.nodeConf
@ -52,7 +53,7 @@ class AppConfigTest extends BitcoinSUnitTest {
} }
it must "have the same DB path" in { it must "have the same DB path" in {
val conf = BitcoinSAppConfig.getTestConfig() val conf = BitcoinSTestAppConfig.getTestConfig()
val chainConf = conf.chainConf val chainConf = conf.chainConf
val walletConf = conf.walletConf val walletConf = conf.walletConf
val nodeConf = conf.nodeConf val nodeConf = conf.nodeConf
@ -61,7 +62,7 @@ class AppConfigTest extends BitcoinSUnitTest {
} }
it must "have distinct databases" in { it must "have distinct databases" in {
val conf = BitcoinSAppConfig.getTestConfig() val conf = BitcoinSTestAppConfig.getTestConfig()
val chainConf = conf.chainConf val chainConf = conf.chainConf
val walletConf = conf.walletConf val walletConf = conf.walletConf
val nodeConf = conf.nodeConf val nodeConf = conf.nodeConf
@ -70,7 +71,7 @@ class AppConfigTest extends BitcoinSUnitTest {
} }
it must "be able to write to distinct databases" in { it must "be able to write to distinct databases" in {
implicit val config = BitcoinSAppConfig.getTestConfig() implicit val config = BitcoinSTestAppConfig.getTestConfig()
val chainConf = config.chainConf val chainConf = config.chainConf
val walletConf = config.walletConf val walletConf = config.walletConf
val nodeConf = config.nodeConf val nodeConf = config.nodeConf

View File

@ -21,7 +21,7 @@ import org.bitcoins.core.hd.HDCoin
import org.bitcoins.core.hd.HDChainType import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.hd.HDPurposes import org.bitcoins.core.hd.HDPurposes
import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.server.BitcoinSAppConfig
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import akka.actor.ActorSystem import akka.actor.ActorSystem
@ -39,6 +39,7 @@ import org.bitcoins.wallet.models.AccountDb
import _root_.akka.actor.Address import _root_.akka.actor.Address
import org.scalatest.compatible.Assertion import org.scalatest.compatible.Assertion
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
import org.bitcoins.testkit.BitcoinSTestAppConfig
class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
@ -189,7 +190,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
private def testAccountType(purpose: HDPurpose): Future[Assertion] = { private def testAccountType(purpose: HDPurpose): Future[Assertion] = {
val confOverride = configForPurpose(purpose) val confOverride = configForPurpose(purpose)
implicit val conf: WalletAppConfig = implicit val conf: WalletAppConfig =
BitcoinSAppConfig.getTestConfig(confOverride) BitcoinSTestAppConfig.getTestConfig(confOverride)
val vectors = purpose match { val vectors = purpose match {
case HDPurposes.Legacy => legacyVectors case HDPurposes.Legacy => legacyVectors

View File

@ -68,6 +68,9 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
// it should not be confirmed // it should not be confirmed
utxosPostAdd <- wallet.listUtxos() utxosPostAdd <- wallet.listUtxos()
_ = assert(utxosPostAdd.length == 1) _ = assert(utxosPostAdd.length == 1)
_ <- wallet
.getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet _ <- wallet
.getConfirmedBalance() .getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin)) .map(confirmed => assert(confirmed == 0.bitcoin))

View File

@ -19,6 +19,12 @@ import org.bitcoins.core.crypto.AesIV
// what do we do if seed exists? error if they aren't equal? // what do we do if seed exists? error if they aren't equal?
object WalletStorage extends BitcoinSLogger { object WalletStorage extends BitcoinSLogger {
/** Checks if a wallet seed exists in datadir */
def seedExists()(implicit config: WalletAppConfig): Boolean = {
val seedPath = config.datadir.resolve(ENCRYPTED_SEED_FILE_NAME)
Files.exists(seedPath)
}
private[wallet] val ENCRYPTED_SEED_FILE_NAME: String = private[wallet] val ENCRYPTED_SEED_FILE_NAME: String =
"encrypted_bitcoin-s_seed.json" "encrypted_bitcoin-s_seed.json"