From 30e6d7030f560c46c5e9eaf3c36029652091e6d1 Mon Sep 17 00:00:00 2001 From: Torkel Rogstad Date: Wed, 10 Jul 2019 13:33:17 +0200 Subject: [PATCH] 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 --- .scalafmt.conf | 1 + app/cli-test/cli-test.sbt | 3 + app/cli/cli.sbt | 9 + .../src/main/scala/org/bitcoins/cli/Cli.scala | 238 ++++++++++++++++++ .../scala/org/bitcoins/cli/CliReaders.scala | 44 ++++ .../org/bitcoins/picklers/Picklers.scala | 20 ++ app/server-test/server-test.sbt | 3 + app/server/server.sbt | 9 + .../src/main/resources/application.conf | 4 + .../bitcoins/server}/BitcoinSAppConfig.scala | 63 +---- .../org/bitcoins/server/ChainRoutes.scala | 33 +++ .../scala/org/bitcoins/server/HttpError.scala | 13 + .../main/scala/org/bitcoins/server/Main.scala | 134 ++++++++++ .../org/bitcoins/server/NodeRoutes.scala | 21 ++ .../scala/org/bitcoins/server/Server.scala | 131 ++++++++++ .../bitcoins/server/ServerJsonModels.scala | 47 ++++ .../org/bitcoins/server/ServerRoute.scala | 6 + .../org/bitcoins/server/WalletRoutes.scala | 60 +++++ bitcoin-s-docs/docs.sbt | 3 + .../rpc/common/BlockchainRpcTest.scala | 3 +- .../rpc/jsonmodels/BlockchainResult.scala | 14 +- build.sbt | 77 ++++-- .../chain/blockchain/ChainHandlerTest.scala | 9 +- .../bitcoins/chain/pow/BitcoinPowTest.scala | 8 +- .../chain/validation/TipValidationTest.scala | 2 +- inThisBuild.sbt | 7 +- .../bitcoins/node/NodeWithWalletTest.scala | 2 +- .../bitcoins/node/networking/ClientTest.scala | 11 +- project/Deps.scala | 93 ++++--- .../testkit/BitcoinSTestAppConfig.scala | 65 +++++ .../testkit/chain/ChainUnitTest.scala | 6 +- .../bitcoins/testkit/node/NodeUnitTest.scala | 7 +- .../testkit/wallet/BitcoinSWalletTest.scala | 5 +- .../bitcoins/testkit/db/AppConfigTest.scala | 13 +- .../bitcoins/wallet/TrezorAddressTest.scala | 5 +- .../wallet/WalletIntegrationTest.scala | 3 + .../org/bitcoins/wallet/WalletStorage.scala | 6 + 37 files changed, 1015 insertions(+), 163 deletions(-) create mode 100644 app/cli-test/cli-test.sbt create mode 100644 app/cli/cli.sbt create mode 100644 app/cli/src/main/scala/org/bitcoins/cli/Cli.scala create mode 100644 app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala create mode 100644 app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala create mode 100644 app/server-test/server-test.sbt create mode 100644 app/server/server.sbt create mode 100644 app/server/src/main/resources/application.conf rename {testkit/src/main/scala/org/bitcoins/testkit => app/server/src/main/scala/org/bitcoins/server}/BitcoinSAppConfig.scala (58%) create mode 100644 app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/HttpError.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/Main.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/Server.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/ServerRoute.scala create mode 100644 app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala create mode 100644 testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index 844960b1ed..ae2a0fc824 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,4 @@ +version = "1.5.1" # See Documentation at https://scalameta.org/scalafmt/#Configuration maxColumn=80 docstrings=ScalaDoc diff --git a/app/cli-test/cli-test.sbt b/app/cli-test/cli-test.sbt new file mode 100644 index 0000000000..4b448243ad --- /dev/null +++ b/app/cli-test/cli-test.sbt @@ -0,0 +1,3 @@ +name := "bitcoin-s-cli-test" + +publish / skip := true diff --git a/app/cli/cli.sbt b/app/cli/cli.sbt new file mode 100644 index 0000000000..b6d3932aaf --- /dev/null +++ b/app/cli/cli.sbt @@ -0,0 +1,9 @@ +name := "bitcoin-s-cli" + +libraryDependencies ++= Deps.cli + +publish / skip := true + +graalVMNativeImageOptions += "-H:EnableURLProtocols=http" + +enablePlugins(JavaAppPackaging, GraalVMNativeImagePlugin) diff --git a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala new file mode 100644 index 0000000000..5045b94501 --- /dev/null +++ b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala @@ -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]("") + .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.") + } +} diff --git a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala new file mode 100644 index 0000000000..3c312c5163 --- /dev/null +++ b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala @@ -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)) + } +} diff --git a/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala b/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala new file mode 100644 index 0000000000..750a0b057e --- /dev/null +++ b/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala @@ -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) + +} diff --git a/app/server-test/server-test.sbt b/app/server-test/server-test.sbt new file mode 100644 index 0000000000..be05e06715 --- /dev/null +++ b/app/server-test/server-test.sbt @@ -0,0 +1,3 @@ +name := "bitcoin-s-server-test" + +publish / skip := true diff --git a/app/server/server.sbt b/app/server/server.sbt new file mode 100644 index 0000000000..ab18208778 --- /dev/null +++ b/app/server/server.sbt @@ -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 diff --git a/app/server/src/main/resources/application.conf b/app/server/src/main/resources/application.conf new file mode 100644 index 0000000000..a44be61dcd --- /dev/null +++ b/app/server/src/main/resources/application.conf @@ -0,0 +1,4 @@ +akka { + loggers = ["akka.event.slf4j.Slf4jLogger"] + loglevel = "DEBUG" +} \ No newline at end of file diff --git a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSAppConfig.scala b/app/server/src/main/scala/org/bitcoins/server/BitcoinSAppConfig.scala similarity index 58% rename from testkit/src/main/scala/org/bitcoins/testkit/BitcoinSAppConfig.scala rename to app/server/src/main/scala/org/bitcoins/server/BitcoinSAppConfig.scala index 543e9de52f..090ebedce9 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSAppConfig.scala +++ b/app/server/src/main/scala/org/bitcoins/server/BitcoinSAppConfig.scala @@ -1,4 +1,4 @@ -package org.bitcoins.testkit +package org.bitcoins.server import com.typesafe.config.Config import org.bitcoins.wallet.config.WalletAppConfig @@ -6,8 +6,6 @@ import org.bitcoins.node.config.NodeAppConfig import org.bitcoins.chain.config.ChainAppConfig import scala.concurrent.ExecutionContext import scala.concurrent.Future -import java.nio.file.Files -import com.typesafe.config.ConfigFactory /** * 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 */ - lazy val config = { + lazy val config: Config = { assert(chainConf.config == nodeConf.config) assert(nodeConf.config == walletConf.config) @@ -75,61 +73,4 @@ object BitcoinSAppConfig { implicit def toNodeConf(conf: BitcoinSAppConfig): NodeAppConfig = 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) - } - } diff --git a/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala new file mode 100644 index 0000000000..401ced51d6 --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/ChainRoutes.scala @@ -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) + } + } + } + +} diff --git a/app/server/src/main/scala/org/bitcoins/server/HttpError.scala b/app/server/src/main/scala/org/bitcoins/server/HttpError.scala new file mode 100644 index 0000000000..36e4c299a9 --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/HttpError.scala @@ -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 +} diff --git a/app/server/src/main/scala/org/bitcoins/server/Main.scala b/app/server/src/main/scala/org/bitcoins/server/Main.scala new file mode 100644 index 0000000000..e8b3343a61 --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/Main.scala @@ -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) + } +} diff --git a/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala new file mode 100644 index 0000000000..79b222bd32 --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala @@ -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") + } + } +} diff --git a/app/server/src/main/scala/org/bitcoins/server/Server.scala b/app/server/src/main/scala/org/bitcoins/server/Server.scala new file mode 100644 index 0000000000..3fc8177471 --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/Server.scala @@ -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) + } +} diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala new file mode 100644 index 0000000000..ec55d350f3 --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -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")) + } + } + +} diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerRoute.scala b/app/server/src/main/scala/org/bitcoins/server/ServerRoute.scala new file mode 100644 index 0000000000..8d11ca13dd --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/ServerRoute.scala @@ -0,0 +1,6 @@ +package org.bitcoins.server +import akka.http.scaladsl.server.StandardRoute + +trait ServerRoute { + def handleCommand: PartialFunction[ServerCommand, StandardRoute] +} diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala new file mode 100644 index 0000000000..689a349f1b --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -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) + + } + } + } + + } +} diff --git a/bitcoin-s-docs/docs.sbt b/bitcoin-s-docs/docs.sbt index 9c42be5a48..5ef0e3fa31 100644 --- a/bitcoin-s-docs/docs.sbt +++ b/bitcoin-s-docs/docs.sbt @@ -34,4 +34,7 @@ buildInfoPackage := "org.bitcoins.docs" // Mdoc end /////// +Test / bloopGenerate := None +Compile / bloopGenerate := None + libraryDependencies ++= Deps.docs diff --git a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/BlockchainRpcTest.scala b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/BlockchainRpcTest.scala index baee73baba..e1ae5f7016 100644 --- a/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/BlockchainRpcTest.scala +++ b/bitcoind-rpc-test/src/test/scala/org/bitcoins/rpc/common/BlockchainRpcTest.scala @@ -9,6 +9,7 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.testkit.util.BitcoindRpcTest import scala.concurrent.Future +import org.bitcoins.core.config.RegTest class BlockchainRpcTest extends BitcoindRpcTest { @@ -75,7 +76,7 @@ class BlockchainRpcTest extends BitcoindRpcTest { info <- client.getBlockChainInfo bestHash <- client.getBestBlockHash } yield { - assert(info.chain == "regtest") + assert(info.chain == RegTest) assert(info.softforks.length >= 3) assert(info.bip9_softforks.keySet.size >= 2) assert(info.bestblockhash == bestHash) diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/jsonmodels/BlockchainResult.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/jsonmodels/BlockchainResult.scala index ddfa09aa2c..af1a4cb639 100644 --- a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/jsonmodels/BlockchainResult.scala +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/jsonmodels/BlockchainResult.scala @@ -5,6 +5,7 @@ import org.bitcoins.core.currency.Bitcoins import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.wallet.fee.BitcoinFeeUnit +import org.bitcoins.core.config.NetworkParameters sealed abstract class BlockchainResult @@ -51,7 +52,7 @@ case class GetBlockWithTransactionsResult( extends BlockchainResult case class GetBlockChainInfoResult( - chain: String, + chain: NetworkParameters, blocks: Int, headers: Int, bestblockhash: DoubleSha256DigestBE, @@ -106,6 +107,7 @@ case class GetBlockHeaderResult( previousblockhash: Option[DoubleSha256DigestBE], nextblockhash: Option[DoubleSha256DigestBE]) extends BlockchainResult { + def blockHeader: BlockHeader = { //prevblockhash is only empty if we have the genesis block @@ -118,11 +120,11 @@ case class GetBlockHeaderResult( } } BlockHeader(version = Int32(version), - previousBlockHash = prevHash.flip, - merkleRootHash = merkleroot.flip, - time = time, - nBits = bits, - nonce = nonce) + previousBlockHash = prevHash.flip, + merkleRootHash = merkleroot.flip, + time = time, + nBits = bits, + nonce = nonce) } } diff --git a/build.sbt b/build.sbt index b21d0a2220..6330df9343 100644 --- a/build.sbt +++ b/build.sbt @@ -77,18 +77,6 @@ lazy val commonSettings = List( assemblyOption in assembly := (assemblyOption in assembly).value .copy(includeScala = false), 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 Test / parallelExecution := !(Properties.isMac && sys.props .get("CI") @@ -133,6 +121,8 @@ lazy val bitcoins = project secp256k1jni, chain, chainTest, + cli, + cliTest, core, coreTest, dbCommons, @@ -143,8 +133,11 @@ lazy val bitcoins = project eclairRpcTest, node, nodeTest, + picklers, wallet, walletTest, + walletServer, + walletServerTest, testkit, scripts, zmq @@ -152,7 +145,6 @@ lazy val bitcoins = project .settings(commonSettings: _*) // crossScalaVersions must be set to Nil on the aggregating project .settings(crossScalaVersions := Nil) - .settings(libraryDependencies ++= Deps.root) .enablePlugins(ScalaUnidocPlugin, GitVersioning) .settings( // we modify the unidoc task to move the generated Scaladocs into the @@ -292,6 +284,48 @@ lazy val coreTest = project ) .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 chain = project .in(file("chain")) @@ -428,6 +462,8 @@ lazy val testkit = project .settings(commonSettings: _*) .dependsOn( core % testAndCompile, + walletServer, + cli, chain, bitcoindRpc, eclairRpc, @@ -492,19 +528,6 @@ lazy val scripts = project 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 def dbFlywaySettings(dbName: String): List[Setting[_]] = { @@ -554,4 +577,4 @@ def dbFlywaySettings(dbName: String): List[Setting[_]] = { } } -publishArtifact in bitcoins := false \ No newline at end of file +publishArtifact in bitcoins := false diff --git a/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala b/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala index 7c52065ac8..9212dbdc30 100644 --- a/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala +++ b/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala @@ -20,7 +20,8 @@ import org.scalatest.{Assertion, FutureOutcome} import play.api.libs.json.Json import scala.concurrent.Future -import org.bitcoins.testkit.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig +import org.bitcoins.testkit.BitcoinSTestAppConfig class ChainHandlerTest extends ChainUnitTest { @@ -30,8 +31,10 @@ class ChainHandlerTest extends ChainUnitTest { // we're working with mainnet data implicit override lazy val appConfig: ChainAppConfig = { - val memoryDb = BitcoinSAppConfig.configWithMemoryDb( - Some(BitcoinSAppConfig.ProjectType.Chain)) + import BitcoinSTestAppConfig.ProjectType + + val memoryDb = + BitcoinSTestAppConfig.configWithMemoryDb(Some(ProjectType.Chain)) mainnetAppConfig.withOverrides(memoryDb) } diff --git a/chain-test/src/test/scala/org/bitcoins/chain/pow/BitcoinPowTest.scala b/chain-test/src/test/scala/org/bitcoins/chain/pow/BitcoinPowTest.scala index 25d86d3b09..ebc817855d 100644 --- a/chain-test/src/test/scala/org/bitcoins/chain/pow/BitcoinPowTest.scala +++ b/chain-test/src/test/scala/org/bitcoins/chain/pow/BitcoinPowTest.scala @@ -9,15 +9,17 @@ import org.bitcoins.testkit.chain.{ChainTestUtil, ChainUnitTest} import org.scalatest.FutureOutcome import scala.concurrent.Future -import org.bitcoins.testkit.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig +import org.bitcoins.testkit.BitcoinSTestAppConfig class BitcoinPowTest extends ChainUnitTest { override type FixtureParam = ChainFixture implicit override lazy val appConfig: ChainAppConfig = { - val memoryDb = BitcoinSAppConfig.configWithMemoryDb( - Some(BitcoinSAppConfig.ProjectType.Chain)) + import BitcoinSTestAppConfig.ProjectType + val memoryDb = + BitcoinSTestAppConfig.configWithMemoryDb(Some(ProjectType.Chain)) mainnetAppConfig.withOverrides(memoryDb) } diff --git a/chain-test/src/test/scala/org/bitcoins/chain/validation/TipValidationTest.scala b/chain-test/src/test/scala/org/bitcoins/chain/validation/TipValidationTest.scala index 34718f3ddc..34ad7d4f65 100644 --- a/chain-test/src/test/scala/org/bitcoins/chain/validation/TipValidationTest.scala +++ b/chain-test/src/test/scala/org/bitcoins/chain/validation/TipValidationTest.scala @@ -18,7 +18,7 @@ import org.scalatest.{Assertion, FutureOutcome} import scala.concurrent.Future import org.bitcoins.chain.config.ChainAppConfig import com.typesafe.config.ConfigFactory -import org.bitcoins.testkit.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig class TipValidationTest extends ChainUnitTest { diff --git a/inThisBuild.sbt b/inThisBuild.sbt index 5722be83e4..2ad9891028 100644 --- a/inThisBuild.sbt +++ b/inThisBuild.sbt @@ -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" diff --git a/node-test/src/test/scala/org/bitcoins/node/NodeWithWalletTest.scala b/node-test/src/test/scala/org/bitcoins/node/NodeWithWalletTest.scala index cde9ddc3c9..0192b171ad 100644 --- a/node-test/src/test/scala/org/bitcoins/node/NodeWithWalletTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/NodeWithWalletTest.scala @@ -7,7 +7,7 @@ import org.bitcoins.chain.models.BlockHeaderDAO import org.bitcoins.node.config.NodeAppConfig import org.bitcoins.node.models.Peer import org.scalatest.FutureOutcome -import org.bitcoins.testkit.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.testkit.wallet.BitcoinSWalletTest diff --git a/node-test/src/test/scala/org/bitcoins/node/networking/ClientTest.scala b/node-test/src/test/scala/org/bitcoins/node/networking/ClientTest.scala index ddcb1546f6..1adf1a947a 100644 --- a/node-test/src/test/scala/org/bitcoins/node/networking/ClientTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/networking/ClientTest.scala @@ -5,7 +5,8 @@ import akka.testkit.{TestActorRef, TestProbe} import org.bitcoins.node.models.Peer import org.bitcoins.node.networking.peer.PeerMessageReceiver 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.node.NodeTestUtil import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil @@ -25,19 +26,21 @@ class ClientTest with BeforeAndAfterAll { implicit private val config: BitcoinSAppConfig = - BitcoinSAppConfig.getTestConfig() + BitcoinSTestAppConfig.getTestConfig() implicit private val chainConf = config.chainConf implicit private val nodeConf = config.nodeConf 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 => NodeTestUtil.getBitcoindPeer(bitcoind) } - lazy val bitcoindRpc2F = BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum) + lazy val bitcoindRpc2F = + BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum) lazy val bitcoindPeer2F = bitcoindRpcF.map { bitcoind => NodeTestUtil.getBitcoindPeer(bitcoind) diff --git a/project/Deps.scala b/project/Deps.scala index 608fd8f3e1..a1351bd77c 100644 --- a/project/Deps.scala +++ b/project/Deps.scala @@ -25,8 +25,16 @@ object Deps { val akkaActorV = akkaStreamv val slickV = "3.3.1" val sqliteV = "3.27.2.1" - val uJsonV = "0.7.1" 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 { @@ -37,6 +45,8 @@ object Deps { val akkaHttp = "com.typesafe.akka" %% "akka-http" % V.akkav 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 akkaLog = "com.typesafe.akka" %% "akka-slf4j" % V.akkaStreamv + val playJson = "com.typesafe.play" %% "play-json" % V.playv 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 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 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 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 ammonite = Compile.ammonite % "test" val playJson = Compile.playJson % "test" val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % V.akkaActorV withSources () withJavadoc () val scalameter = "com.storm-enroute" %% "scalameter" % V.scalameterV % "test" withSources () withJavadoc () } - val root = List( - Test.ammonite - ) - val chain = List( - Compile.slf4j, - Test.ammonite + Compile.slf4j ) val chainTest = List( - Test.ammonite, Test.logback ) val core = List( Compile.bouncycastle, Compile.scodec, - Compile.slf4j, - Test.ammonite + Compile.slf4j ) val secp256k1jni = List( Compile.nativeLoader, - Test.junitInterface, - Test.ammonite + Test.junitInterface ) val coreTest = List( @@ -110,7 +123,6 @@ object Deps { Test.logback, Test.scalaTest, Test.spray, - Test.ammonite, Test.playJson ) @@ -119,8 +131,7 @@ object Deps { Compile.slf4j, Test.logback, Test.scalacheck, - Test.scalaTest, - Test.ammonite + Test.scalaTest ) val bitcoindRpc = List( @@ -128,8 +139,7 @@ object Deps { Compile.akkaStream, Compile.playJson, Compile.slf4j, - Compile.typesafeConfig, - Test.ammonite + Compile.typesafeConfig ) val bitcoindRpcTest = List( @@ -138,29 +148,43 @@ object Deps { Test.logback, Test.scalaTest, Test.scalacheck, - Test.async, - Test.ammonite + Test.async ) val bench = List( "org.slf4j" % "slf4j-api" % V.slf4j withSources () withJavadoc (), - Compile.logback, - Test.ammonite + Compile.logback ) val dbCommons = List( Compile.slick, Compile.sqlite, - Compile.slickHikari, - Test.ammonite + Compile.slickHikari + ) + + 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( Compile.akkaHttp, Compile.akkaStream, Compile.playJson, - Compile.slf4j, - Test.ammonite + Compile.slf4j ) val eclairRpcTest = List( @@ -168,8 +192,7 @@ object Deps { Test.akkaStream, Test.logback, Test.scalaTest, - Test.scalacheck, - Test.ammonite + Test.scalacheck ) val node = List( @@ -178,23 +201,20 @@ object Deps { Compile.joda, Compile.slick, Compile.slickHikari, - Compile.sqlite, - Test.ammonite + Compile.sqlite ) val nodeTest = List( Test.akkaTestkit, Test.logback, - Test.scalaTest, - Test.ammonite + Test.scalaTest ) val testkit = List( Compile.slf4j, Compile.scalacheck, Compile.scalaTest, - Test.akkaTestkit, - Test.ammonite + Test.akkaTestkit ) val scripts = List( @@ -203,18 +223,15 @@ object Deps { ) val wallet = List( - Test.ammonite, Compile.uJson ) val walletTest = List( Test.logback, - Test.akkaTestkit, - Test.ammonite + Test.akkaTestkit ) val docs = List( - Compile.ammonite, Compile.logback, Test.scalaTest, Test.logback diff --git a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala new file mode 100644 index 0000000000..c41abff47a --- /dev/null +++ b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala @@ -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) + } +} diff --git a/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala index c668339e09..992896cd06 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala @@ -20,7 +20,6 @@ import org.bitcoins.testkit.chain.fixture._ import org.bitcoins.testkit.fixtures.BitcoinSFixture import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil import org.bitcoins.zmq.ZMQSubscriber -import org.bitcoins.testkit.BitcoinSAppConfig import org.scalatest._ import play.api.libs.json.{JsError, JsSuccess, Json} import scodec.bits.ByteVector @@ -29,6 +28,7 @@ import scala.annotation.tailrec import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import org.bitcoins.db.AppConfig +import org.bitcoins.testkit.BitcoinSTestAppConfig trait ChainUnitTest extends org.scalatest.fixture.AsyncFlatSpec @@ -46,7 +46,7 @@ trait ChainUnitTest implicit lazy val chainParam: ChainParams = appConfig.chain implicit lazy val appConfig: ChainAppConfig = - BitcoinSAppConfig.getTestConfig() + BitcoinSTestAppConfig.getTestConfig() /** * Behaves exactly like the default conf, execpt @@ -54,7 +54,7 @@ trait ChainUnitTest */ lazy val mainnetAppConfig: ChainAppConfig = { val mainnetConf = ConfigFactory.parseString("bitcoin-s.network = mainnet") - BitcoinSAppConfig.getTestConfig(mainnetConf) + BitcoinSTestAppConfig.getTestConfig(mainnetConf) } override def beforeAll(): Unit = { diff --git a/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala index e8f5c81fba..4a0be3dae2 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala @@ -14,8 +14,8 @@ import org.bitcoins.node.networking.peer.{ PeerMessageSender } import org.bitcoins.rpc.client.common.BitcoindRpcClient -import org.bitcoins.testkit.BitcoinSAppConfig -import org.bitcoins.testkit.BitcoinSAppConfig._ +import org.bitcoins.server.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig._ import org.bitcoins.testkit.chain.ChainUnitTest import org.bitcoins.testkit.fixtures.BitcoinSFixture import org.bitcoins.testkit.node.fixture.SpvNodeConnectedWithBitcoind @@ -29,6 +29,7 @@ import org.scalatest.{ import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} +import org.bitcoins.testkit.BitcoinSTestAppConfig trait NodeUnitTest extends BitcoinSFixture @@ -57,7 +58,7 @@ trait NodeUnitTest /** Wallet config with data directory set to user temp directory */ implicit protected lazy val config: BitcoinSAppConfig = - BitcoinSAppConfig.getTestConfig() + BitcoinSTestAppConfig.getTestConfig() implicit lazy val np: NetworkParameters = config.nodeConf.network diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index 1f5a01eda5..be4803a31d 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -19,9 +19,10 @@ import org.scalatest._ import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.concurrent.{ExecutionContext, Future} import org.bitcoins.db.AppConfig -import org.bitcoins.testkit.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig import com.typesafe.config.Config import com.typesafe.config.ConfigFactory +import org.bitcoins.testkit.BitcoinSTestAppConfig trait BitcoinSWalletTest extends fixture.AsyncFlatSpec @@ -35,7 +36,7 @@ trait BitcoinSWalletTest /** Wallet config with data directory set to user temp directory */ implicit protected lazy val config: BitcoinSAppConfig = - BitcoinSAppConfig.getTestConfig() + BitcoinSTestAppConfig.getTestConfig() /** Timeout for async operations */ protected val timeout: FiniteDuration = 10.seconds diff --git a/testkit/src/test/scala/org/bitcoins/testkit/db/AppConfigTest.scala b/testkit/src/test/scala/org/bitcoins/testkit/db/AppConfigTest.scala index c0533e552f..0d3a1ea888 100644 --- a/testkit/src/test/scala/org/bitcoins/testkit/db/AppConfigTest.scala +++ b/testkit/src/test/scala/org/bitcoins/testkit/db/AppConfigTest.scala @@ -2,8 +2,8 @@ package org.bitcoins.testkit.db import org.bitcoins.testkit.util.BitcoinSUnitTest import org.bitcoins.testkit.Implicits._ -import org.bitcoins.testkit.BitcoinSAppConfig -import org.bitcoins.testkit.BitcoinSAppConfig._ +import org.bitcoins.server.BitcoinSAppConfig +import org.bitcoins.server.BitcoinSAppConfig._ import com.typesafe.config.ConfigFactory import org.bitcoins.core.config.TestNet3 import org.bitcoins.chain.models.BlockHeaderDAO @@ -26,6 +26,7 @@ import org.bitcoins.db.SQLiteTableInfo import slick.jdbc.SQLiteProfile.api._ import org.bitcoins.db.CRUD import java.nio.file.Files +import org.bitcoins.testkit.BitcoinSTestAppConfig class AppConfigTest extends BitcoinSUnitTest { @@ -38,7 +39,7 @@ class AppConfigTest extends BitcoinSUnitTest { val networkOverride = ConfigFactory.parseString("bitcoin-s.network = testnet3") - val config = BitcoinSAppConfig.getTestConfig(networkOverride) + val config = BitcoinSTestAppConfig.getTestConfig(networkOverride) val chainConf = config.chainConf val walletConf = config.walletConf val nodeConf = config.nodeConf @@ -52,7 +53,7 @@ class AppConfigTest extends BitcoinSUnitTest { } it must "have the same DB path" in { - val conf = BitcoinSAppConfig.getTestConfig() + val conf = BitcoinSTestAppConfig.getTestConfig() val chainConf = conf.chainConf val walletConf = conf.walletConf val nodeConf = conf.nodeConf @@ -61,7 +62,7 @@ class AppConfigTest extends BitcoinSUnitTest { } it must "have distinct databases" in { - val conf = BitcoinSAppConfig.getTestConfig() + val conf = BitcoinSTestAppConfig.getTestConfig() val chainConf = conf.chainConf val walletConf = conf.walletConf val nodeConf = conf.nodeConf @@ -70,7 +71,7 @@ class AppConfigTest extends BitcoinSUnitTest { } 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 walletConf = config.walletConf val nodeConf = config.nodeConf diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala index 78fab9e678..91804ef3e9 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala @@ -21,7 +21,7 @@ import org.bitcoins.core.hd.HDCoin import org.bitcoins.core.hd.HDChainType import org.bitcoins.core.hd.HDPurposes 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.ConfigFactory import akka.actor.ActorSystem @@ -39,6 +39,7 @@ import org.bitcoins.wallet.models.AccountDb import _root_.akka.actor.Address import org.scalatest.compatible.Assertion import scala.concurrent.ExecutionContext +import org.bitcoins.testkit.BitcoinSTestAppConfig class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { @@ -189,7 +190,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { private def testAccountType(purpose: HDPurpose): Future[Assertion] = { val confOverride = configForPurpose(purpose) implicit val conf: WalletAppConfig = - BitcoinSAppConfig.getTestConfig(confOverride) + BitcoinSTestAppConfig.getTestConfig(confOverride) val vectors = purpose match { case HDPurposes.Legacy => legacyVectors diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala index 28372ced45..f255eb2e9e 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala @@ -68,6 +68,9 @@ class WalletIntegrationTest extends BitcoinSWalletTest { // it should not be confirmed utxosPostAdd <- wallet.listUtxos() _ = assert(utxosPostAdd.length == 1) + _ <- wallet + .getConfirmedBalance() + .map(confirmed => assert(confirmed == 0.bitcoin)) _ <- wallet .getConfirmedBalance() .map(confirmed => assert(confirmed == 0.bitcoin)) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/WalletStorage.scala b/wallet/src/main/scala/org/bitcoins/wallet/WalletStorage.scala index d54c35346c..768f7776e7 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/WalletStorage.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/WalletStorage.scala @@ -19,6 +19,12 @@ import org.bitcoins.core.crypto.AesIV // what do we do if seed exists? error if they aren't equal? 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 = "encrypted_bitcoin-s_seed.json"