mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
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:
parent
2632e1a628
commit
30e6d7030f
@ -1,3 +1,4 @@
|
||||
version = "1.5.1"
|
||||
# See Documentation at https://scalameta.org/scalafmt/#Configuration
|
||||
maxColumn=80
|
||||
docstrings=ScalaDoc
|
||||
|
3
app/cli-test/cli-test.sbt
Normal file
3
app/cli-test/cli-test.sbt
Normal file
@ -0,0 +1,3 @@
|
||||
name := "bitcoin-s-cli-test"
|
||||
|
||||
publish / skip := true
|
9
app/cli/cli.sbt
Normal file
9
app/cli/cli.sbt
Normal file
@ -0,0 +1,9 @@
|
||||
name := "bitcoin-s-cli"
|
||||
|
||||
libraryDependencies ++= Deps.cli
|
||||
|
||||
publish / skip := true
|
||||
|
||||
graalVMNativeImageOptions += "-H:EnableURLProtocols=http"
|
||||
|
||||
enablePlugins(JavaAppPackaging, GraalVMNativeImagePlugin)
|
238
app/cli/src/main/scala/org/bitcoins/cli/Cli.scala
Normal file
238
app/cli/src/main/scala/org/bitcoins/cli/Cli.scala
Normal 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.")
|
||||
}
|
||||
}
|
44
app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala
Normal file
44
app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala
Normal 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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
3
app/server-test/server-test.sbt
Normal file
3
app/server-test/server-test.sbt
Normal file
@ -0,0 +1,3 @@
|
||||
name := "bitcoin-s-server-test"
|
||||
|
||||
publish / skip := true
|
9
app/server/server.sbt
Normal file
9
app/server/server.sbt
Normal 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
|
4
app/server/src/main/resources/application.conf
Normal file
4
app/server/src/main/resources/application.conf
Normal file
@ -0,0 +1,4 @@
|
||||
akka {
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
loglevel = "DEBUG"
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
134
app/server/src/main/scala/org/bitcoins/server/Main.scala
Normal file
134
app/server/src/main/scala/org/bitcoins/server/Main.scala
Normal 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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
131
app/server/src/main/scala/org/bitcoins/server/Server.scala
Normal file
131
app/server/src/main/scala/org/bitcoins/server/Server.scala
Normal 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)
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.bitcoins.server
|
||||
import akka.http.scaladsl.server.StandardRoute
|
||||
|
||||
trait ServerRoute {
|
||||
def handleCommand: PartialFunction[ServerCommand, StandardRoute]
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -34,4 +34,7 @@ buildInfoPackage := "org.bitcoins.docs"
|
||||
// Mdoc end
|
||||
///////
|
||||
|
||||
Test / bloopGenerate := None
|
||||
Compile / bloopGenerate := None
|
||||
|
||||
libraryDependencies ++= Deps.docs
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
77
build.sbt
77
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
|
||||
publishArtifact in bitcoins := false
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user