mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-23 06:45:21 +01:00
Add proper parsing of bitcoin.conf, rework auth credentials (#478)
* Add proper parsing of bitcoin.conf, remove datadir Somewhat recently a new config format was introduced in Bitcoin Core. Options can now be specified under section headers ([regtest], [main], [test]), as well as with network prefixes (regtest.rpcport, main.prune) and the traditional format. In this commit we introduce a new type BitcoindConfig that parses this format. We also make the necessary changes in the bitcoind/Eclair RPC clients to accomodate this new type. We also remove the datadir field from BitcoindAuthCredentials. This is not strictly necessary to connect to a bitcoind, so this is a bad separation of concerns. The datadir field is instead moved into the BitcoindInstance. Finally we go over some places in tests and test utils and assert that we are operating in the user temporary directory. This is a safety measure so that other developers/users won't do the same mistake I did while working on this and accidentally blow up their $HOME/.bitcoin directory. * Add BitcoindRpcClient.fromDatadir * Address code review from Chris
This commit is contained in:
parent
c07ae36076
commit
fb178eb295
20 changed files with 1086 additions and 359 deletions
|
@ -9,13 +9,19 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
|||
import org.bitcoins.testkit.util.BitcoindRpcTest
|
||||
|
||||
import scala.io.Source
|
||||
import akka.stream.StreamTcpException
|
||||
import java.nio.file.Paths
|
||||
import scala.util.Properties
|
||||
import org.bitcoins.rpc.config.BitcoindConfig
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials
|
||||
import org.bitcoins.rpc.util.RpcUtil
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import java.net.URI
|
||||
|
||||
class BitcoindInstanceTest extends BitcoindRpcTest {
|
||||
|
||||
private val source =
|
||||
Source.fromURL(getClass.getResource("/sample-bitcoin.conf"))
|
||||
|
||||
private val sampleConf: Seq[String] = {
|
||||
val source = Source.fromURL(getClass.getResource("/sample-bitcoin.conf"))
|
||||
source.getLines.toSeq
|
||||
}
|
||||
|
||||
|
@ -28,8 +34,106 @@ class BitcoindInstanceTest extends BitcoindRpcTest {
|
|||
pw.close()
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {}
|
||||
|
||||
def addDatadirAndWrite(conf: BitcoindConfig): BitcoindConfig = {
|
||||
val tempDir = Files.createTempDirectory("")
|
||||
val confWithDatadir = conf.datadir match {
|
||||
case None =>
|
||||
conf.withOption("datadir", tempDir.toString)
|
||||
case Some(value) => conf
|
||||
}
|
||||
val tempfile = Paths.get(Properties.tmpDir, "bitcoin.conf")
|
||||
BitcoindRpcTestUtil.writeConfigToFile(confWithDatadir)
|
||||
confWithDatadir
|
||||
}
|
||||
|
||||
behavior of "BitcoindInstance"
|
||||
|
||||
it should "start a bitcoind with cookie based authentication" in {
|
||||
val confStr = s"""
|
||||
|regtest=1
|
||||
|daemon=1
|
||||
|port=${RpcUtil.randomPort}
|
||||
|rpcport=${RpcUtil.randomPort}
|
||||
""".stripMargin
|
||||
|
||||
val conf = addDatadirAndWrite(BitcoindConfig(confStr))
|
||||
val instance = BitcoindInstance.fromConfig(conf)
|
||||
assert(
|
||||
instance.authCredentials
|
||||
.isInstanceOf[BitcoindAuthCredentials.CookieBased])
|
||||
for {
|
||||
cli <- BitcoindRpcTestUtil.startedBitcoindRpcClient(instance,
|
||||
clientAccum =
|
||||
clientAccum)
|
||||
_ <- cli.getBalance
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
it should "start a bitcoind with user and password based authentication" in {
|
||||
val confStr = s"""
|
||||
|daemon=1
|
||||
|regtest=1
|
||||
|rpcuser=foobar
|
||||
|rpcpassword=barfoo
|
||||
|port=${RpcUtil.randomPort}
|
||||
|rpcport=${RpcUtil.randomPort}
|
||||
""".stripMargin
|
||||
|
||||
val conf = addDatadirAndWrite(BitcoindConfig(confStr))
|
||||
val instance = BitcoindInstance.fromConfig(conf)
|
||||
assert(
|
||||
instance.authCredentials
|
||||
.isInstanceOf[BitcoindAuthCredentials.PasswordBased])
|
||||
for {
|
||||
cli <- BitcoindRpcTestUtil.startedBitcoindRpcClient(instance,
|
||||
clientAccum =
|
||||
clientAccum)
|
||||
_ <- cli.getBalance
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
// the values in this conf was generated by executing
|
||||
// rpcauth.py from Bicoin Core like this:
|
||||
//
|
||||
// ❯ ./rpcauth.py bitcoin-s strong_password
|
||||
// String to be appended to bitcoin.conf:
|
||||
// rpcauth=bitcoin-s:6d7580be1deb4ae52bc4249871845b09$82b282e7c6493f6982a5a7af9fbb1b671bab702e2f31bbb1c016bb0ea1cc27ca
|
||||
// Your password:
|
||||
// strong_password
|
||||
it should "start a bitcoind with auth based authentication" in {
|
||||
val port = RpcUtil.randomPort
|
||||
val rpcPort = RpcUtil.randomPort
|
||||
val confStr = s"""
|
||||
|daemon=1
|
||||
|rpcauth=bitcoin-s:6d7580be1deb4ae52bc4249871845b09$$82b282e7c6493f6982a5a7af9fbb1b671bab702e2f31bbb1c016bb0ea1cc27ca
|
||||
|regtest=1
|
||||
|port=${RpcUtil.randomPort}
|
||||
|rpcport=${RpcUtil.randomPort}
|
||||
""".stripMargin
|
||||
|
||||
val conf = addDatadirAndWrite(BitcoindConfig(confStr))
|
||||
val authCredentials =
|
||||
BitcoindAuthCredentials.PasswordBased(username = "bitcoin-s",
|
||||
password = "strong_password")
|
||||
val instance =
|
||||
BitcoindInstance(
|
||||
network = RegTest,
|
||||
uri = new URI(s"http://localhost:$port"),
|
||||
rpcUri = new URI(s"http://localhost:$rpcPort"),
|
||||
authCredentials = authCredentials,
|
||||
datadir = conf.datadir.get
|
||||
)
|
||||
|
||||
for {
|
||||
cli <- BitcoindRpcTestUtil.startedBitcoindRpcClient(instance,
|
||||
clientAccum =
|
||||
clientAccum)
|
||||
_ <- cli.getBalance
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
it should "parse a bitcoin.conf file, start bitcoind, mine some blocks and quit" in {
|
||||
val instance = BitcoindInstance.fromDatadir(datadir.toFile)
|
||||
val client = new BitcoindRpcClient(instance)
|
||||
|
@ -39,6 +143,15 @@ class BitcoindInstanceTest extends BitcoindRpcTest {
|
|||
_ <- client.generate(101)
|
||||
balance <- client.getBalance
|
||||
_ <- BitcoindRpcTestUtil.stopServers(Vector(client))
|
||||
_ <- client.getBalance
|
||||
.map { balance =>
|
||||
logger.error(s"Got unexpected balance: $balance")
|
||||
fail("Was able to connect to bitcoind after shutting down")
|
||||
}
|
||||
.recover {
|
||||
case _: StreamTcpException =>
|
||||
()
|
||||
}
|
||||
} yield assert(balance > Bitcoins(0))
|
||||
|
||||
}
|
||||
|
|
|
@ -109,8 +109,9 @@ class TestRpcUtilTest extends BitcoindRpcTest {
|
|||
it should "create a temp bitcoin directory when creating a DaemonInstance, and then delete it" in {
|
||||
val instance =
|
||||
BitcoindRpcTestUtil.instance(RpcUtil.randomPort, RpcUtil.randomPort)
|
||||
val dir = instance.authCredentials.datadir
|
||||
val dir = instance.datadir
|
||||
assert(dir.isDirectory)
|
||||
assert(dir.getPath().startsWith(scala.util.Properties.tmpDir))
|
||||
assert(
|
||||
dir.listFiles.contains(new File(dir.getAbsolutePath + "/bitcoin.conf")))
|
||||
BitcoindRpcTestUtil.deleteTmpDir(dir)
|
||||
|
@ -131,8 +132,8 @@ class TestRpcUtilTest extends BitcoindRpcTest {
|
|||
it should "be able to create a connected node pair with more than 100 blocks and then delete them" in {
|
||||
for {
|
||||
(client1, client2) <- BitcoindRpcTestUtil.createNodePair()
|
||||
_ = assert(client1.getDaemon.authCredentials.datadir.isDirectory)
|
||||
_ = assert(client2.getDaemon.authCredentials.datadir.isDirectory)
|
||||
_ = assert(client1.getDaemon.datadir.isDirectory)
|
||||
_ = assert(client2.getDaemon.datadir.isDirectory)
|
||||
|
||||
nodes <- client1.getAddedNodeInfo(client2.getDaemon.uri)
|
||||
_ = assert(nodes.nonEmpty)
|
||||
|
@ -143,8 +144,8 @@ class TestRpcUtilTest extends BitcoindRpcTest {
|
|||
_ = assert(count2 > 100)
|
||||
_ <- BitcoindRpcTestUtil.deleteNodePair(client1, client2)
|
||||
} yield {
|
||||
assert(!client1.getDaemon.authCredentials.datadir.exists)
|
||||
assert(!client2.getDaemon.authCredentials.datadir.exists)
|
||||
assert(!client1.getDaemon.datadir.exists)
|
||||
assert(!client2.getDaemon.datadir.exists)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
|||
import org.bitcoins.testkit.util.BitcoindRpcTest
|
||||
|
||||
import scala.concurrent.Future
|
||||
import java.nio.file.Path
|
||||
|
||||
class MempoolRpcTest extends BitcoindRpcTest {
|
||||
lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
|
||||
|
@ -27,20 +28,15 @@ class MempoolRpcTest extends BitcoindRpcTest {
|
|||
case (client, otherClient) =>
|
||||
val defaultConfig = BitcoindRpcTestUtil.standardConfig
|
||||
|
||||
val datadirValue = {
|
||||
val datadir: Path = {
|
||||
val tempDirPrefix = null // because java APIs are bad
|
||||
val tempdirPath = Files.createTempDirectory(tempDirPrefix).toString
|
||||
ConfigValueFactory.fromAnyRef(tempdirPath)
|
||||
Files.createTempDirectory(tempDirPrefix)
|
||||
}
|
||||
|
||||
// walletbroadcast must be turned off for a transaction to be abondonable
|
||||
val noBroadcastValue = ConfigValueFactory.fromAnyRef(0)
|
||||
|
||||
// connecting clients once they are started takes forever for some reason
|
||||
val configNoBroadcast =
|
||||
defaultConfig
|
||||
.withValue("walletbroadcast", noBroadcastValue)
|
||||
.withValue("datadir", datadirValue)
|
||||
.withOption("datadir", datadir.toString())
|
||||
.withOption("walletbroadcast", 0.toString)
|
||||
|
||||
val _ = BitcoindRpcTestUtil.writeConfigToFile(configNoBroadcast)
|
||||
|
||||
|
@ -193,7 +189,7 @@ class MempoolRpcTest extends BitcoindRpcTest {
|
|||
(client, _) <- clientsF
|
||||
regTest = {
|
||||
val regTest =
|
||||
new File(client.getDaemon.authCredentials.datadir + "/regtest")
|
||||
new File(client.getDaemon.datadir + "/regtest")
|
||||
assert(regTest.isDirectory)
|
||||
assert(!regTest.list().contains("mempool.dat"))
|
||||
regTest
|
||||
|
|
|
@ -60,7 +60,7 @@ class WalletRpcTest extends BitcoindRpcTest {
|
|||
for {
|
||||
(client, _, _) <- clientsF
|
||||
result <- {
|
||||
val datadir = client.getDaemon.authCredentials.datadir
|
||||
val datadir = client.getDaemon.datadir
|
||||
client.dumpWallet(datadir + "/test.dat")
|
||||
}
|
||||
} yield {
|
||||
|
@ -87,11 +87,11 @@ class WalletRpcTest extends BitcoindRpcTest {
|
|||
for {
|
||||
(client, _, _) <- clientsF
|
||||
_ <- {
|
||||
val datadir = client.getDaemon.authCredentials.datadir
|
||||
val datadir = client.getDaemon.datadir
|
||||
client.backupWallet(datadir + "/backup.dat")
|
||||
}
|
||||
} yield {
|
||||
val datadir = client.getDaemon.authCredentials.datadir
|
||||
val datadir = client.getDaemon.datadir
|
||||
val file = new File(datadir + "/backup.dat")
|
||||
assert(file.exists)
|
||||
assert(file.isFile)
|
||||
|
@ -369,8 +369,7 @@ class WalletRpcTest extends BitcoindRpcTest {
|
|||
_ <- client.importPrivKey(ecPrivateKey, rescan = false)
|
||||
key <- client.dumpPrivKey(address)
|
||||
result <- client
|
||||
.dumpWallet(
|
||||
client.getDaemon.authCredentials.datadir + "/wallet_dump.dat")
|
||||
.dumpWallet(client.getDaemon.datadir + "/wallet_dump.dat")
|
||||
} yield {
|
||||
assert(key == ecPrivateKey)
|
||||
val reader = new Scanner(result.filename)
|
||||
|
@ -426,7 +425,7 @@ class WalletRpcTest extends BitcoindRpcTest {
|
|||
(client, _, _) <- clientsF
|
||||
walletClient <- walletClientF
|
||||
address <- client.getNewAddress
|
||||
walletFile = client.getDaemon.authCredentials.datadir + "/client_wallet.dat"
|
||||
walletFile = client.getDaemon.datadir + "/client_wallet.dat"
|
||||
|
||||
fileResult <- client.dumpWallet(walletFile)
|
||||
_ <- walletClient.walletPassphrase(password, 1000)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package org.bitcoins.rpc.config
|
||||
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials.CookieBased
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials.PasswordBased
|
||||
import org.bitcoins.core.config.RegTest
|
||||
|
||||
class BitcoindAuthCredentialsTest extends BitcoinSUnitTest {
|
||||
it must "handle cookie based auth" in {
|
||||
val confStr = """
|
||||
|regtest=1
|
||||
""".stripMargin
|
||||
val conf = BitcoindConfig(confStr)
|
||||
val auth = BitcoindAuthCredentials.fromConfig(conf)
|
||||
val cookie = auth match {
|
||||
case cookie: CookieBased => cookie
|
||||
case _: PasswordBased =>
|
||||
fail("got password based")
|
||||
}
|
||||
|
||||
assert(conf.network == RegTest)
|
||||
assert(cookie.cookiePath.toString().contains("regtest"))
|
||||
}
|
||||
|
||||
it must "default to password based auth" in {
|
||||
val confStr = """
|
||||
|regtest=1
|
||||
|rpcuser=foo
|
||||
|rpcpassword=bar
|
||||
""".stripMargin
|
||||
val conf = BitcoindConfig(confStr)
|
||||
val auth = BitcoindAuthCredentials.fromConfig(conf)
|
||||
|
||||
val pass = auth match {
|
||||
case _: CookieBased => fail("got cookie")
|
||||
case pass: PasswordBased => pass
|
||||
}
|
||||
|
||||
assert(conf.network == RegTest)
|
||||
assert(pass.password == "bar")
|
||||
assert(pass.username == "foo")
|
||||
}
|
||||
|
||||
it must "handle password based auth" in {
|
||||
val confStr = """
|
||||
|regtest=1
|
||||
|rpcuser=foo
|
||||
|rpcpassword=bar
|
||||
""".stripMargin
|
||||
|
||||
val conf = BitcoindConfig(confStr)
|
||||
BitcoindAuthCredentials.fromConfig(conf) match {
|
||||
case _: CookieBased => fail
|
||||
case PasswordBased(username, password) =>
|
||||
assert(username == "foo")
|
||||
assert(password == "bar")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package org.bitcoins.rpc.config
|
||||
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.config.TestNet3
|
||||
import org.bitcoins.core.config.MainNet
|
||||
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
||||
import org.bitcoins.testkit.util.BitcoindRpcTest
|
||||
|
||||
class BitcoindConfigTest extends BitcoinSUnitTest {
|
||||
|
||||
it must "have to/fromString symmetry" in {
|
||||
val conf = BitcoindRpcTestUtil.standardConfig
|
||||
val confStr = conf.toWriteableString
|
||||
val otherConf = BitcoindConfig(confStr)
|
||||
val otherConfStr = otherConf.toWriteableString
|
||||
assert(confStr == otherConfStr)
|
||||
}
|
||||
|
||||
it must "parse networks" in {
|
||||
val conf = BitcoindConfig("""
|
||||
|regtest=1
|
||||
""".stripMargin)
|
||||
assert(conf.network == RegTest)
|
||||
}
|
||||
|
||||
it must "prioritize a prefixed option" in {
|
||||
val confStr = """
|
||||
|regtest=1
|
||||
|
|
||||
|rpcport=2000
|
||||
|regtest.rpcport=3000
|
||||
|
|
||||
|[regtest]
|
||||
|rpcport=4000
|
||||
""".stripMargin.split("\n")
|
||||
|
||||
val conf = BitcoindConfig(confStr)
|
||||
assert(conf.rpcport == 3000)
|
||||
assert(conf.network == RegTest)
|
||||
}
|
||||
|
||||
it must "avoid to get prefixed options in section headers" in {
|
||||
val confStr = """
|
||||
|regtest=1
|
||||
|
|
||||
|rpcport=2000
|
||||
|
|
||||
|[regtest]
|
||||
|rpcport=4000
|
||||
|
|
||||
|regtest.rpcport=3000
|
||||
""".stripMargin.split("\n")
|
||||
|
||||
val conf = BitcoindConfig(confStr)
|
||||
assert(conf.rpcport == 4000)
|
||||
assert(conf.network == RegTest)
|
||||
}
|
||||
|
||||
it must "avoid getting options for the wrong network" in {
|
||||
val confStr = """
|
||||
|testnet=1
|
||||
|
|
||||
|[regtest]
|
||||
|rpcport=4000
|
||||
|
|
||||
|regtest.rpcport=3000
|
||||
""".stripMargin.split("\n")
|
||||
|
||||
val conf = BitcoindConfig(confStr)
|
||||
assert(conf.rpcport == TestNet3.rpcPort)
|
||||
assert(conf.network == TestNet3)
|
||||
}
|
||||
|
||||
it must "get options with multiple header sections" in {
|
||||
val confStr = """
|
||||
|testnet=1
|
||||
|
|
||||
|[test]
|
||||
|rpcuser=username
|
||||
|rpcport=3000
|
||||
|
|
||||
|[regtest]
|
||||
|rpcport=4000
|
||||
|
|
||||
|[main]
|
||||
|rpcport=1000
|
||||
""".stripMargin.split("\n")
|
||||
|
||||
val conf = BitcoindConfig(confStr)
|
||||
assert(conf.rpcport == 3000)
|
||||
assert(conf.network == TestNet3)
|
||||
assert(conf.username.contains("username"))
|
||||
}
|
||||
|
||||
it must "fallback to default values" in {
|
||||
val conf = BitcoindConfig.empty
|
||||
assert(conf.network == MainNet)
|
||||
assert(conf.rpcport == MainNet.rpcPort)
|
||||
assert(conf.password.isEmpty)
|
||||
assert(conf.username.isEmpty)
|
||||
}
|
||||
|
||||
it must "get options with multiple header sections when our section is the last" in {
|
||||
val confStr = """
|
||||
|regtest=1
|
||||
|
|
||||
|[test]
|
||||
|rpcport=3000
|
||||
|
|
||||
|[main]
|
||||
|rpcport=1000
|
||||
|
||||
|[regtest]
|
||||
|rpcport=4000
|
||||
|rpcuser=username
|
||||
""".stripMargin.split("\n")
|
||||
|
||||
val conf = BitcoindConfig(confStr)
|
||||
assert(conf.rpcport == 4000)
|
||||
assert(conf.network == RegTest)
|
||||
assert(conf.username.contains("username"))
|
||||
|
||||
}
|
||||
|
||||
it must "have a default config in test utils" in {
|
||||
val conf = BitcoindRpcTestUtil.standardConfig
|
||||
assert(conf.username.isDefined)
|
||||
assert(conf.password.isDefined)
|
||||
assert(conf.zmqpubhashblock.isDefined)
|
||||
assert(conf.zmqpubhashtx.isDefined)
|
||||
assert(conf.zmqpubrawblock.isDefined)
|
||||
assert(conf.zmqpubrawtx.isDefined)
|
||||
assert({
|
||||
conf.rpcUri // cal by name
|
||||
true
|
||||
})
|
||||
|
||||
assert({
|
||||
conf.uri // cal by name
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import scala.async.Async.{async, await}
|
|||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.util.Properties
|
||||
import org.bitcoins.rpc.client.common.BitcoindVersion
|
||||
|
||||
class BitcoindV16RpcClientTest extends BitcoindRpcTest {
|
||||
lazy val clientsF: Future[(BitcoindV16RpcClient, BitcoindV16RpcClient)] =
|
||||
|
@ -27,6 +28,15 @@ class BitcoindV16RpcClientTest extends BitcoindRpcTest {
|
|||
|
||||
behavior of "BitoindV16RpcClient"
|
||||
|
||||
it should "be able to start a V16 bitcoind" in {
|
||||
for {
|
||||
(client, otherClient) <- clientsF
|
||||
} yield {
|
||||
assert(client.version == BitcoindVersion.V16)
|
||||
assert(otherClient.version == BitcoindVersion.V16)
|
||||
}
|
||||
}
|
||||
|
||||
it should "be able to sign a raw transaction" in {
|
||||
for {
|
||||
(client, otherClient) <- clientsF
|
||||
|
|
|
@ -2,6 +2,9 @@ package org.bitcoins.rpc.client.common
|
|||
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.rpc.config.BitcoindInstance
|
||||
import scala.concurrent.Future
|
||||
import java.io.File
|
||||
import org.bitcoins.rpc.config.BitcoindConfig
|
||||
|
||||
/**
|
||||
* This class is not guaranteed to be compatible with any particular
|
||||
|
@ -35,6 +38,20 @@ class BitcoindRpcClient(val instance: BitcoindInstance)(
|
|||
|
||||
}
|
||||
|
||||
object BitcoindRpcClient {
|
||||
|
||||
/**
|
||||
* Constructs a RPC client from the given datadir, or
|
||||
* the default datadir if no directory is provided
|
||||
*/
|
||||
def fromDatadir(datadir: File = BitcoindConfig.DEFAULT_DATADIR)(
|
||||
implicit system: ActorSystem): BitcoindRpcClient = {
|
||||
val instance = BitcoindInstance.fromDatadir(datadir)
|
||||
val cli = new BitcoindRpcClient(instance)
|
||||
cli
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait BitcoindVersion
|
||||
|
||||
object BitcoindVersion {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.bitcoins.rpc.client.common
|
||||
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
|
@ -19,9 +18,13 @@ import play.api.libs.json._
|
|||
|
||||
import scala.concurrent._
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.io.Source
|
||||
import scala.sys.process._
|
||||
import scala.util.{Failure, Success}
|
||||
import java.nio.file.Files
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials.CookieBased
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials.PasswordBased
|
||||
import java.nio.file.Path
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials
|
||||
|
||||
/**
|
||||
* This is the base trait for Bitcoin Core
|
||||
|
@ -38,16 +41,20 @@ trait Client extends BitcoinSLogger {
|
|||
/**
|
||||
* The log file of the Bitcoin Core daemon
|
||||
*/
|
||||
def logFile: File = {
|
||||
lazy val logFile: Path = {
|
||||
|
||||
val prefix = instance.network match {
|
||||
case MainNet => ""
|
||||
case TestNet3 => "/testnet"
|
||||
case RegTest => "/regtest"
|
||||
case TestNet3 => "testnet"
|
||||
case RegTest => "regtest"
|
||||
}
|
||||
val datadir = instance.authCredentials.datadir
|
||||
new File(datadir + prefix + "/debug.log")
|
||||
instance.datadir.toPath.resolve(prefix).resolve("debug.log")
|
||||
}
|
||||
|
||||
/** The configuration file of the Bitcoin Core daemon */
|
||||
lazy val confFile: Path =
|
||||
instance.datadir.toPath.resolve("bitcoin.conf")
|
||||
|
||||
protected implicit val system: ActorSystem
|
||||
protected implicit val materializer: ActorMaterializer =
|
||||
ActorMaterializer.create(system)
|
||||
|
@ -88,10 +95,12 @@ trait Client extends BitcoinSLogger {
|
|||
|
||||
val binaryPath = instance.binary.getAbsolutePath
|
||||
val cmd = List(binaryPath,
|
||||
"-datadir=" + instance.authCredentials.datadir,
|
||||
"-datadir=" + instance.datadir,
|
||||
"-rpcport=" + instance.rpcUri.getPort,
|
||||
"-port=" + instance.uri.getPort)
|
||||
logger.debug(s"starting bitcoind")
|
||||
|
||||
logger.debug(
|
||||
s"starting bitcoind with datadir ${instance.datadir} and binary path $binaryPath")
|
||||
val _ = Process(cmd).run()
|
||||
|
||||
def isStartedF: Future[Boolean] = {
|
||||
|
@ -106,19 +115,55 @@ trait Client extends BitcoinSLogger {
|
|||
started.future
|
||||
}
|
||||
|
||||
val started = AsyncUtil.retryUntilSatisfiedF(() => isStartedF,
|
||||
duration = 1.seconds,
|
||||
maxTries = 60)
|
||||
// if we're doing cookie based authentication, we might attempt
|
||||
// to read the cookie file before it's written. this ensures
|
||||
// we avoid that
|
||||
val awaitCookie: BitcoindAuthCredentials => Future[Unit] = {
|
||||
case cookie: CookieBased =>
|
||||
val cookieExistsF =
|
||||
AsyncUtil.retryUntilSatisfied(Files.exists(cookie.cookiePath))
|
||||
cookieExistsF.onComplete {
|
||||
case Failure(exception) =>
|
||||
logger.error(s"Cookie filed was never created! $exception")
|
||||
case _: Success[_] =>
|
||||
}
|
||||
cookieExistsF
|
||||
case _: PasswordBased => Future.successful(())
|
||||
|
||||
}
|
||||
|
||||
val started = {
|
||||
for {
|
||||
_ <- awaitCookie(instance.authCredentials)
|
||||
_ <- AsyncUtil.retryUntilSatisfiedF(() => isStartedF,
|
||||
duration = 1.seconds,
|
||||
maxTries = 60)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
started.onComplete {
|
||||
case Success(_) => logger.debug(s"started bitcoind")
|
||||
case Failure(exc) =>
|
||||
if (instance.network != MainNet) {
|
||||
logger.info(
|
||||
s"Could not start bitcoind instance! Message: ${exc.getMessage}")
|
||||
logger.info(s"Log lines:")
|
||||
val logLines = Source.fromFile(logFile).getLines()
|
||||
logLines.foreach(logger.info)
|
||||
logger.info(
|
||||
s"Could not start bitcoind instance! Message: ${exc.getMessage}")
|
||||
// When we're unable to start bitcoind that's most likely
|
||||
// either a configuration error or bug in Bitcoin-S. In either
|
||||
// case it's much easier to debug this with conf and logs
|
||||
// dumped somewhere. Especially in tests this is
|
||||
// convenient, as our test framework deletes the data directories
|
||||
// of our instances. We don't want to do this on mainnet,
|
||||
// as both the logs and conf file most likely contain sensitive
|
||||
// information
|
||||
if (network != MainNet) {
|
||||
val tempfile = Files.createTempFile("bitcoind-log-", ".dump")
|
||||
val logfile = Files.readAllBytes(logFile)
|
||||
Files.write(tempfile, logfile)
|
||||
logger.info(s"Dumped debug.log to $tempfile")
|
||||
|
||||
val otherTempfile = Files.createTempFile("bitcoin-conf-", ".dump")
|
||||
val conffile = Files.readAllBytes(confFile)
|
||||
Files.write(otherTempfile, conffile)
|
||||
logger.info(s"Dumped bitcoin.conf to $otherTempfile")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,45 +1,100 @@
|
|||
package org.bitcoins.rpc.config
|
||||
|
||||
import java.io.File
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
import java.nio.file.Paths
|
||||
import org.bitcoins.core.config.TestNet3
|
||||
import org.bitcoins.core.config.MainNet
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import java.nio.file.Files
|
||||
import org.bitcoins.core.config.NetworkParameters
|
||||
|
||||
/**
|
||||
* Created by chris on 5/2/17.
|
||||
* This trait contains the information we need to authenticate
|
||||
* to a `bitcoind` node.
|
||||
*/
|
||||
sealed trait BitcoindAuthCredentials {
|
||||
|
||||
/** The directory where our bitcoin.conf file is located */
|
||||
def datadir: File
|
||||
|
||||
/** rpcusername field in our bitcoin.conf file */
|
||||
def username: String
|
||||
|
||||
/** rpcpassword field in our bitcoin.conf file */
|
||||
def password: String
|
||||
|
||||
def rpcPort: Int
|
||||
def username: String
|
||||
}
|
||||
|
||||
object BitcoindAuthCredentials {
|
||||
private case class BitcoindAuthCredentialsImpl(
|
||||
username: String,
|
||||
password: String,
|
||||
rpcPort: Int,
|
||||
datadir: File)
|
||||
extends BitcoindAuthCredentials
|
||||
object BitcoindAuthCredentials extends BitcoinSLogger {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
def apply(
|
||||
/**
|
||||
* Authenticate by providing a username and password.
|
||||
* If you are connecting to a local `bitcoind` you
|
||||
* should instead use cookie based authentication.
|
||||
* If you are connecting to a remote `bitcoind`, you
|
||||
* should use the Bitcoin Core-provided script
|
||||
* `rpcauth.py` to generate credentials. This will
|
||||
* give you a `rpcauth=...` string you can put in
|
||||
* your remote `bitcoind` configuration, as well as
|
||||
* a set of `rpcuser=...` and `rpcpassword=...` you
|
||||
* can put in your local `bitcoin.conf` configuration
|
||||
* file or provide directly to this class.
|
||||
*
|
||||
* @see [[https://github.com/bitcoin/bitcoin/tree/master/share/rpcauth rpcauth.py]],
|
||||
* canonical Python script provided by Bitcoin Core to generate the
|
||||
* auth credentials.
|
||||
*/
|
||||
case class PasswordBased(
|
||||
username: String,
|
||||
password: String,
|
||||
rpcPort: Int): BitcoindAuthCredentials = {
|
||||
val defaultDataDir = new File(System.getProperty("user.home") + "/.bitcoin")
|
||||
BitcoindAuthCredentials(username, password, rpcPort, defaultDataDir)
|
||||
password: String
|
||||
) extends BitcoindAuthCredentials
|
||||
|
||||
/**
|
||||
* Authenticate by providing a cookie file
|
||||
* found in the `bitcoind` data directory.
|
||||
* This is the most secure as well as user
|
||||
* friendly way of authenticating, but it
|
||||
* is not always suitable for situtations
|
||||
* where the `bitcoind` instance is on a
|
||||
* remote server.
|
||||
*/
|
||||
case class CookieBased(
|
||||
network: NetworkParameters,
|
||||
datadir: File = BitcoindConfig.DEFAULT_DATADIR)
|
||||
extends BitcoindAuthCredentials {
|
||||
|
||||
lazy private[bitcoins] val cookiePath = {
|
||||
val middleSegment = network match {
|
||||
case TestNet3 => "testnet3"
|
||||
case MainNet => ""
|
||||
case RegTest => "regtest"
|
||||
|
||||
}
|
||||
Paths.get(datadir.toString, middleSegment, ".cookie")
|
||||
}
|
||||
|
||||
/**
|
||||
* The cookie is a string looking like
|
||||
* `__cookie__:AUTO_GENERATED_PASSWORD`
|
||||
*/
|
||||
def cookie: String = {
|
||||
if (Files.exists(cookiePath)) {
|
||||
val cookieLines = Files.readAllLines(cookiePath).asScala
|
||||
cookieLines.head
|
||||
} else {
|
||||
throw new RuntimeException(s"Could not find $cookiePath!")
|
||||
}
|
||||
}
|
||||
|
||||
def username: String = cookie.split(":").head
|
||||
def password: String = cookie.split(":").last
|
||||
|
||||
}
|
||||
|
||||
def apply(
|
||||
username: String,
|
||||
password: String,
|
||||
rpcPort: Int,
|
||||
datadir: File): BitcoindAuthCredentials = {
|
||||
BitcoindAuthCredentialsImpl(username, password, rpcPort, datadir)
|
||||
def fromConfig(config: BitcoindConfig): BitcoindAuthCredentials = {
|
||||
val datadir = config.datadir.getOrElse(BitcoindConfig.DEFAULT_DATADIR)
|
||||
val username = config.username
|
||||
val password = config.password
|
||||
(username, password) match {
|
||||
case (Some(user), Some(pass)) =>
|
||||
PasswordBased(user, pass)
|
||||
case (_, _) =>
|
||||
CookieBased(config.network, datadir = datadir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,324 @@
|
|||
package org.bitcoins.rpc.config
|
||||
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
import org.bitcoins.core.config._
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import scala.util.Properties
|
||||
import java.nio.file.Paths
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* This class represents a parsed `bitcoin.conf` file. It
|
||||
* respects the different ways of writing options in
|
||||
* `bitcoin.conf`: Raw options, network-prefixed options
|
||||
* and options within network sections. It also tries to
|
||||
* conform to the way Bitcoin Core gives precedence to the
|
||||
* different properties.
|
||||
*
|
||||
* Not all options are exposed from this class. We only
|
||||
* expose those that are of relevance when making RPC
|
||||
* requests.
|
||||
*
|
||||
* @see https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md
|
||||
*/
|
||||
abstract class BitcoindConfig extends BitcoinSLogger {
|
||||
private[bitcoins] def lines: Seq[String]
|
||||
|
||||
/**
|
||||
* Converts the config back to a string that can be written
|
||||
* to file, and passed to `bitcoind`
|
||||
*/
|
||||
lazy val toWriteableString: String = lines.mkString("\n")
|
||||
|
||||
/** The optional index of the first header section encountered */
|
||||
private lazy val firstHeaderSectionIndex: Option[Int] = {
|
||||
val indices =
|
||||
List(RegTest, TestNet3, MainNet).map(headerSectionIndex).flatten
|
||||
if (indices.nonEmpty) Some(indices.min) else None
|
||||
}
|
||||
|
||||
/** The optional index of the header section for the given network */
|
||||
private def headerSectionIndex(network: NetworkParameters): Option[Int] =
|
||||
lines.indexOf(s"[${networkString(network)}]") match {
|
||||
case -1 => None
|
||||
case other: Int => Some(other)
|
||||
}
|
||||
|
||||
private def networkString(network: NetworkParameters) =
|
||||
network match {
|
||||
case MainNet => "main"
|
||||
case RegTest => "regtest"
|
||||
case TestNet3 => "test"
|
||||
}
|
||||
|
||||
/** The networks we're _not_ on */
|
||||
private lazy val otherNetworks = network match {
|
||||
case MainNet => List("test", "regtest")
|
||||
case RegTest => List("main", "test")
|
||||
case TestNet3 => List("main", "regtest")
|
||||
}
|
||||
|
||||
private lazy val ourNetworkString: String = networkString(network)
|
||||
|
||||
/** Splits the provided lines into pairs of keys/values
|
||||
* based on `=`, and then applies the provided
|
||||
* `collect` function on those pairs
|
||||
*/
|
||||
private def collectFrom(lines: Seq[String])(
|
||||
collect: PartialFunction[(String, String), String]): Seq[String] = {
|
||||
|
||||
val splittedPairs = {
|
||||
val splitLines = lines.map(
|
||||
_.split("=")
|
||||
.map(_.trim)
|
||||
.toList)
|
||||
|
||||
splitLines.collect {
|
||||
case h :: t :: _ => h -> t
|
||||
}
|
||||
}
|
||||
|
||||
splittedPairs.collect(collect)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given partial function to all key/value pairs
|
||||
* found in this config
|
||||
*/
|
||||
private val collectAllLines: PartialFunction[(String, String), String] => Seq[
|
||||
String] = collectFrom(lines)(_)
|
||||
|
||||
/** The blockchain network associated with this `bitcoind` config */
|
||||
lazy val network = {
|
||||
val networkStrOpt =
|
||||
collectAllLines {
|
||||
case (network @ ("testnet" | "regtest" | "mainnet"), "1") => network
|
||||
}.lastOption
|
||||
|
||||
val networkOpt = networkStrOpt.flatMap(Networks.fromString)
|
||||
|
||||
(networkOpt, networkStrOpt) match {
|
||||
case (None, Some(badStr)) =>
|
||||
logger.warn(
|
||||
s"'$badStr' is not a valid Bitcoin network! Defaulting to mainnet")
|
||||
case _ =>
|
||||
}
|
||||
|
||||
networkOpt.getOrElse(MainNet)
|
||||
}
|
||||
|
||||
/**
|
||||
* First searches for option prefixed with network,
|
||||
* then section with header
|
||||
* and lastly just raw option
|
||||
*/
|
||||
private[config] def getValue(key: String): Option[String] =
|
||||
readPrefixOpt(key)
|
||||
.orElse(readSectionHeaderOpt(key))
|
||||
.orElse(readRawOpt(key))
|
||||
|
||||
/** Searches the config for a key matching the provided
|
||||
* string that's prefixed by the network we're currently
|
||||
* on
|
||||
*/
|
||||
private def readPrefixOpt(key: String): Option[String] = {
|
||||
val prefixedOptKey = s"$ourNetworkString.$key"
|
||||
val collect = firstHeaderSectionIndex match {
|
||||
case None => collectAllLines(_)
|
||||
case Some(index) => collectFrom(lines.take(index))(_)
|
||||
}
|
||||
collect {
|
||||
case (`prefixedOptKey`, value) => value
|
||||
}.headOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the config for a key matching the provided
|
||||
* string that's under a section header matching the
|
||||
* network we're on.
|
||||
*/
|
||||
private def readSectionHeaderOpt(key: String): Option[String] = {
|
||||
val startIndexOpt = lines.indexOf(s"[$ourNetworkString]") match {
|
||||
case -1 =>
|
||||
None
|
||||
case other: Int => Some(other)
|
||||
}
|
||||
|
||||
for {
|
||||
startIndex <- startIndexOpt
|
||||
endIndex = {
|
||||
// we're looking for the first section _after_ the
|
||||
// section for our current network
|
||||
val toSearchIn = lines.zipWithIndex.drop(startIndex)
|
||||
toSearchIn
|
||||
.find {
|
||||
case (line, _) =>
|
||||
otherNetworks.exists(net => s"[$net]" == line)
|
||||
}
|
||||
.map {
|
||||
case (_, index) => index
|
||||
}
|
||||
// we got to the end without finding a new section header,
|
||||
// that means we can search to the bottom
|
||||
.getOrElse(lines.length)
|
||||
}
|
||||
result <- {
|
||||
val linesToSearchIn = lines.slice(startIndex, endIndex)
|
||||
val collect = collectFrom(linesToSearchIn)(_)
|
||||
collect {
|
||||
case (`key`, value) => value
|
||||
}.headOption
|
||||
}
|
||||
} yield result
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the config for a key matchinng the provided
|
||||
* string that's _not_ in a section header or prefixed
|
||||
* by a network.
|
||||
*/
|
||||
private def readRawOpt(key: String): Option[String] = {
|
||||
val linesToSearchIn = lines.takeWhile(!_.startsWith("["))
|
||||
val collect = collectFrom(linesToSearchIn)(_)
|
||||
collect {
|
||||
case (`key`, value) => value
|
||||
}.headOption
|
||||
}
|
||||
|
||||
lazy val datadir: Option[File] = getValue("datadir").map(new File(_))
|
||||
|
||||
lazy val username: Option[String] = getValue("rpcuser")
|
||||
lazy val password: Option[String] = getValue("rpcpassword")
|
||||
lazy val zmqpubrawblock: Option[URI] =
|
||||
getValue("zmqpubrawblock").map(new URI(_))
|
||||
lazy val zmqpubrawtx: Option[URI] = getValue("zmqpubrawtx").map(new URI(_))
|
||||
lazy val zmqpubhashblock: Option[URI] =
|
||||
getValue("zmqpubhashblock").map(new URI(_))
|
||||
lazy val zmqpubhashtx: Option[URI] = getValue("zmqpubhashtx").map(new URI(_))
|
||||
|
||||
lazy val port: Int = getValue("port").map(_.toInt).getOrElse(network.port)
|
||||
|
||||
/** Defaults to localhost */
|
||||
lazy val bind: URI = new URI({
|
||||
val baseUrl = getValue("bind").getOrElse("localhost")
|
||||
if (baseUrl.startsWith("http")) baseUrl
|
||||
else "http://" + baseUrl
|
||||
|
||||
})
|
||||
|
||||
lazy val uri: URI = new URI(s"$bind:$port")
|
||||
|
||||
lazy val rpcport: Int =
|
||||
getValue("rpcport").map(_.toInt).getOrElse(network.rpcPort)
|
||||
|
||||
/** Defaults to localhost */
|
||||
lazy val rpcbind: URI = new URI({
|
||||
val baseUrl = getValue("rpcbind").getOrElse("localhost")
|
||||
if (baseUrl.startsWith("http")) baseUrl
|
||||
else "http://" + baseUrl
|
||||
})
|
||||
lazy val rpcUri: URI = new URI(s"$rpcbind:$rpcport")
|
||||
|
||||
/** Creates a new config with the given keys and values appended */
|
||||
def withOption(key: String, value: String): BitcoindConfig = {
|
||||
val ourLines = this.lines
|
||||
new BitcoindConfig {
|
||||
|
||||
def lines: Seq[String] = {
|
||||
val newLine = s"$key=$value"
|
||||
newLine +: ourLines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a new config with the given key and values,
|
||||
* with the given network prefixed to the key
|
||||
*
|
||||
* Old config:
|
||||
* {{{
|
||||
* rpcport=4000
|
||||
* }}}
|
||||
*
|
||||
* New config:
|
||||
* {{{
|
||||
* withOption("rpcport", "2000", MainNet) =
|
||||
* main.rpcport=2000
|
||||
* rpcport=4000
|
||||
* }}}
|
||||
*/
|
||||
def withOption(
|
||||
key: String,
|
||||
value: String,
|
||||
network: NetworkParameters): BitcoindConfig =
|
||||
withOption(key = s"${networkString(network)}.$key", value = value)
|
||||
|
||||
}
|
||||
|
||||
object BitcoindConfig {
|
||||
|
||||
/** The empty `bitcoind` config */
|
||||
lazy val empty: BitcoindConfig = BitcoindConfig("")
|
||||
|
||||
/** Constructs a `bitcoind` config from the given lines */
|
||||
def apply(config: Seq[String]): BitcoindConfig = new BitcoindConfig {
|
||||
val lines: Seq[String] = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a `bitcoind` config from the given string,
|
||||
* by splitting it on newlines
|
||||
*/
|
||||
def apply(config: String): BitcoindConfig =
|
||||
apply(config.split("\n"))
|
||||
|
||||
/** Reads the given path and construct a `bitcoind` config from it */
|
||||
def apply(config: Path): BitcoindConfig = apply(config.toFile)
|
||||
|
||||
/** Reads the given file and construct a `bitcoind` config from it */
|
||||
def apply(config: File): BitcoindConfig = {
|
||||
import scala.collection.JavaConverters._
|
||||
val lines = Files
|
||||
.readAllLines(config.toPath)
|
||||
.iterator()
|
||||
.asScala
|
||||
.toList
|
||||
|
||||
apply(lines)
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a `bitcoin.conf` in the default
|
||||
* data directory, this is read. Otherwise, the
|
||||
* default configuration is returned.
|
||||
*/
|
||||
def fromDefaultDatadir: BitcoindConfig = {
|
||||
if (DEFAULT_CONF_FILE.isFile()) {
|
||||
apply(DEFAULT_CONF_FILE)
|
||||
} else {
|
||||
BitcoindConfig.empty
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://en.bitcoin.it/wiki/Data_directory
|
||||
*/
|
||||
val DEFAULT_DATADIR: File = {
|
||||
val path = if (Properties.isMac) {
|
||||
Paths.get(Properties.userHome,
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Bitcoin")
|
||||
} else {
|
||||
Paths.get(Properties.userHome, ".bitcoin")
|
||||
}
|
||||
path.toFile
|
||||
}
|
||||
|
||||
/** Default location of bitcoind conf file */
|
||||
val DEFAULT_CONF_FILE: File = DEFAULT_DATADIR
|
||||
.toPath()
|
||||
.resolve("bitcoin.conf")
|
||||
.toFile
|
||||
}
|
|
@ -4,20 +4,18 @@ import java.io.File
|
|||
import java.net.URI
|
||||
import java.nio.file.Paths
|
||||
|
||||
import com.typesafe.config._
|
||||
import org.bitcoins.core.config.{NetworkParameters, _}
|
||||
import org.bitcoins.rpc.client.common.BitcoindVersion
|
||||
|
||||
import scala.sys.process._
|
||||
import scala.util.{Failure, Properties, Success, Try}
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
import org.bitcoins.core.config.NetworkParameters
|
||||
import scala.util.Try
|
||||
import java.nio.file.Files
|
||||
|
||||
/**
|
||||
* Created by chris on 4/29/17.
|
||||
*/
|
||||
sealed trait BitcoindInstance {
|
||||
require(
|
||||
rpcUri.getPort == rpcPort,
|
||||
s"RpcUri and the rpcPort in authCredentials are different $rpcUri authcred: $rpcPort")
|
||||
sealed trait BitcoindInstance extends BitcoinSLogger {
|
||||
|
||||
require(binary.exists,
|
||||
s"bitcoind binary path (${binary.getAbsolutePath}) does not exist!")
|
||||
|
@ -29,14 +27,14 @@ sealed trait BitcoindInstance {
|
|||
/** The binary file that should get executed to start Bitcoin Core */
|
||||
def binary: File
|
||||
|
||||
def datadir: File
|
||||
|
||||
def network: NetworkParameters
|
||||
def uri: URI
|
||||
def rpcUri: URI
|
||||
def authCredentials: BitcoindAuthCredentials
|
||||
def zmqConfig: ZmqConfig
|
||||
|
||||
def rpcPort: Int = authCredentials.rpcPort
|
||||
|
||||
def getVersion: BitcoindVersion = {
|
||||
|
||||
val binaryPath = binary.getAbsolutePath
|
||||
|
@ -61,7 +59,8 @@ object BitcoindInstance {
|
|||
rpcUri: URI,
|
||||
authCredentials: BitcoindAuthCredentials,
|
||||
zmqConfig: ZmqConfig,
|
||||
binary: File
|
||||
binary: File,
|
||||
datadir: File
|
||||
) extends BitcoindInstance
|
||||
|
||||
def apply(
|
||||
|
@ -70,14 +69,16 @@ object BitcoindInstance {
|
|||
rpcUri: URI,
|
||||
authCredentials: BitcoindAuthCredentials,
|
||||
zmqConfig: ZmqConfig = ZmqConfig(),
|
||||
binary: File = DEFAULT_BITCOIND_LOCATION
|
||||
binary: File = DEFAULT_BITCOIND_LOCATION,
|
||||
datadir: File = BitcoindConfig.DEFAULT_DATADIR
|
||||
): BitcoindInstance = {
|
||||
BitcoindInstanceImpl(network,
|
||||
uri,
|
||||
rpcUri,
|
||||
authCredentials,
|
||||
zmqConfig = zmqConfig,
|
||||
binary = binary)
|
||||
binary = binary,
|
||||
datadir = datadir)
|
||||
}
|
||||
|
||||
lazy val DEFAULT_BITCOIND_LOCATION: File = {
|
||||
|
@ -87,138 +88,70 @@ object BitcoindInstance {
|
|||
new File(path.trim)
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from Bitcoin Wiki
|
||||
* https://en.bitcoin.it/wiki/Data_directory
|
||||
/** Constructs a `bitcoind` instance from the given datadir, using the
|
||||
* `bitcoin.conf` found within (if any)
|
||||
*
|
||||
* @throws IllegalArgumentException if the given datadir does not exist
|
||||
*/
|
||||
private val DEFAULT_DATADIR =
|
||||
if (Properties.isMac) {
|
||||
Paths.get(Properties.userHome,
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Bitcoin")
|
||||
} else {
|
||||
Paths.get(Properties.userHome, ".bitcoin")
|
||||
}
|
||||
|
||||
private val DEFAULT_CONF_FILE = DEFAULT_DATADIR.resolve("bitcoin.conf")
|
||||
|
||||
def fromDatadir(datadir: File = DEFAULT_DATADIR.toFile): BitcoindInstance = {
|
||||
def fromDatadir(
|
||||
datadir: File = BitcoindConfig.DEFAULT_DATADIR): BitcoindInstance = {
|
||||
require(datadir.exists, s"${datadir.getPath} does not exist!")
|
||||
require(datadir.isDirectory, s"${datadir.getPath} is not a directory!")
|
||||
|
||||
val file = Paths.get(datadir.getAbsolutePath, "bitcoin.conf").toFile
|
||||
fromConfigFile(file)
|
||||
val configPath = Paths.get(datadir.getAbsolutePath, "bitcoin.conf")
|
||||
if (Files.exists(configPath)) {
|
||||
|
||||
val file = configPath.toFile()
|
||||
fromConfigFile(file)
|
||||
} else {
|
||||
fromConfig(
|
||||
BitcoindConfig.empty.withOption("datadir", configPath.toString))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a `bitcoind` from the given config file. If no `datadir` setting
|
||||
* is found, the parent directory to the given file is used.
|
||||
*
|
||||
* @throws IllegalArgumentException if the given config file does not exist
|
||||
*/
|
||||
def fromConfigFile(
|
||||
file: File = DEFAULT_CONF_FILE.toFile): BitcoindInstance = {
|
||||
file: File = BitcoindConfig.DEFAULT_CONF_FILE): BitcoindInstance = {
|
||||
require(file.exists, s"${file.getPath} does not exist!")
|
||||
require(file.isFile, s"${file.getPath} is not a file!")
|
||||
|
||||
val config = ConfigFactory.parseFile(
|
||||
file,
|
||||
ConfigParseOptions.defaults
|
||||
.setSyntax(ConfigSyntax.PROPERTIES)) // bitcoin.conf is not a proper .conf file, uses Java properties=like syntax
|
||||
val conf = BitcoindConfig(file)
|
||||
|
||||
val configWithDatadir =
|
||||
if (config.hasPath("datadir")) {
|
||||
config
|
||||
} else {
|
||||
config.withValue("datadir",
|
||||
ConfigValueFactory.fromAnyRef(file.getParent))
|
||||
}
|
||||
val confWithDatadir = if (conf.datadir.isEmpty) {
|
||||
conf.withOption("datadir", file.getParent.toString)
|
||||
} else {
|
||||
conf
|
||||
}
|
||||
|
||||
fromConfig(configWithDatadir)
|
||||
}
|
||||
|
||||
def fromConfig(config: Config): BitcoindInstance = {
|
||||
val datadirStr = Try(config.getString("datadir"))
|
||||
.getOrElse(
|
||||
throw new IllegalArgumentException(
|
||||
"Provided config does not contain \"datadir\" setting!"))
|
||||
|
||||
val datadir = new File(datadirStr)
|
||||
require(datadir.exists, s"Datadir $datadirStr does not exist!")
|
||||
require(datadir.isDirectory, s"Datadir $datadirStr is not directory")
|
||||
fromConfig(config, datadir)
|
||||
fromConfig(confWithDatadir)
|
||||
}
|
||||
|
||||
/** Constructs a `bitcoind` instance from the given config */
|
||||
def fromConfig(
|
||||
config: Config,
|
||||
datadir: File
|
||||
config: BitcoindConfig
|
||||
): BitcoindInstance = {
|
||||
val network = getNetwork(config)
|
||||
|
||||
val uri = getUri(config, network)
|
||||
val rpcUri = getRpcUri(config, network)
|
||||
val authCredentials = BitcoindAuthCredentials.fromConfig(config)
|
||||
|
||||
val username = config.getString("rpcuser")
|
||||
val password = config.getString("rpcpassword")
|
||||
val authCredentials =
|
||||
BitcoindAuthCredentials(username = username,
|
||||
password = password,
|
||||
rpcPort = rpcUri.getPort,
|
||||
datadir = datadir)
|
||||
|
||||
BitcoindInstance(network,
|
||||
uri,
|
||||
rpcUri,
|
||||
authCredentials,
|
||||
zmqConfig = ZmqConfig.fromConfig(config))
|
||||
}
|
||||
|
||||
private def isSet(config: Config, path: String): Boolean = {
|
||||
Try(config.getInt(path))
|
||||
.map(_ == 1)
|
||||
.getOrElse(false)
|
||||
}
|
||||
|
||||
private def getNetwork(config: Config): BitcoinNetwork = {
|
||||
val isTestnet = isSet(config, path = "testnet")
|
||||
val isRegTest = isSet(config, path = "regtest")
|
||||
|
||||
(isRegTest, isTestnet) match {
|
||||
case (true, true) =>
|
||||
throw new IllegalArgumentException(
|
||||
""""Cannot set both "regtest" and "testnet" options""")
|
||||
case (true, false) => RegTest
|
||||
case (false, true) => TestNet3
|
||||
case (false, false) => MainNet
|
||||
config.datadir match {
|
||||
case None =>
|
||||
BitcoindInstance(config.network,
|
||||
config.uri,
|
||||
config.rpcUri,
|
||||
authCredentials,
|
||||
zmqConfig = ZmqConfig.fromConfig(config))
|
||||
case Some(datadir) =>
|
||||
BitcoindInstance(config.network,
|
||||
config.uri,
|
||||
config.rpcUri,
|
||||
authCredentials,
|
||||
zmqConfig = ZmqConfig.fromConfig(config),
|
||||
datadir = datadir)
|
||||
}
|
||||
}
|
||||
|
||||
private def getUri(config: Config, network: NetworkParameters): URI = {
|
||||
val port = Try(config.getInt("port")).getOrElse(network.port)
|
||||
val host = Try(config.getString("bind")).getOrElse("localhost")
|
||||
val uriT = Try {
|
||||
new URI(s"http://$host:$port")
|
||||
}
|
||||
|
||||
uriT match {
|
||||
case Success(uriSuccess) => uriSuccess
|
||||
case Failure(exception) =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Could not construct URI from host $host and port $port",
|
||||
exception)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def getRpcUri(config: Config, network: NetworkParameters): URI = {
|
||||
val rpcPort = Try(config.getInt("rpcport")).getOrElse(network.rpcPort)
|
||||
val rpcHost = Try(config.getString("rpcbind")).getOrElse("localhost")
|
||||
|
||||
val rpcUriT = Try {
|
||||
new URI(s"http://$rpcHost:$rpcPort")
|
||||
}
|
||||
rpcUriT match {
|
||||
case Success(uriSuccess) => uriSuccess
|
||||
case Failure(exception) =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Could not construct URI from host $rpcHost and port $rpcPort",
|
||||
exception)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package org.bitcoins.rpc.config
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import com.typesafe.config.Config
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
sealed trait ZmqConfig {
|
||||
def hashBlock: Option[URI]
|
||||
def rawBlock: Option[URI]
|
||||
|
@ -43,48 +40,10 @@ object ZmqConfig {
|
|||
rawTx = Some(uri))
|
||||
}
|
||||
|
||||
def fromConfig(config: Config): ZmqConfig =
|
||||
ZmqConfig(hashBlock = hashBlockUri(config),
|
||||
hashTx = hashTxUri(config),
|
||||
rawBlock = rawBlockUri(config),
|
||||
rawTx = rawTxUri(config))
|
||||
|
||||
private val RAW_BLOCK_KEY = "zmqpubrawblock"
|
||||
private val RAW_TX_KEY = "zmqpubrawtx"
|
||||
private val HASH_BLOCK_KEY = "zmqpubhashblock"
|
||||
private val HASH_TX_KEY = "zmqpubhashtx"
|
||||
|
||||
private val ZMQ_CONFIG_KEYS =
|
||||
List(RAW_TX_KEY, RAW_BLOCK_KEY, HASH_TX_KEY, HASH_BLOCK_KEY)
|
||||
|
||||
private def isValidZmqConfigKey(key: String): Boolean =
|
||||
ZMQ_CONFIG_KEYS.contains(key)
|
||||
|
||||
private def getZmqUri(config: Config, path: String): Option[URI] = {
|
||||
require(
|
||||
isValidZmqConfigKey(path),
|
||||
s"$path is not a valid ZMQ config key. Valid keys: ${ZMQ_CONFIG_KEYS.mkString(", ")}")
|
||||
|
||||
if (config.hasPath(path)) {
|
||||
Try(config.getString(path))
|
||||
.map(str => Some(new URI(str)))
|
||||
.getOrElse(throw new IllegalArgumentException(
|
||||
s"$path (${config.getString(path)}) in config is not a valid URI"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
private def rawBlockUri(config: Config): Option[URI] =
|
||||
getZmqUri(config, RAW_BLOCK_KEY)
|
||||
|
||||
private def rawTxUri(config: Config): Option[URI] =
|
||||
getZmqUri(config, RAW_TX_KEY)
|
||||
|
||||
private def hashBlockUri(config: Config): Option[URI] =
|
||||
getZmqUri(config, HASH_BLOCK_KEY)
|
||||
|
||||
private def hashTxUri(config: Config): Option[URI] =
|
||||
getZmqUri(config, HASH_TX_KEY)
|
||||
def fromConfig(config: BitcoindConfig): ZmqConfig =
|
||||
ZmqConfig(hashBlock = config.zmqpubhashblock,
|
||||
hashTx = config.zmqpubhashtx,
|
||||
rawBlock = config.zmqpubrawblock,
|
||||
rawTx = config.zmqpubrawtx)
|
||||
|
||||
}
|
||||
|
|
|
@ -156,6 +156,14 @@ object Networks {
|
|||
val p2pkhNetworkBytes: Seq[ByteVector] = knownNetworks.map(_.p2pkhNetworkByte)
|
||||
val p2shNetworkBytes: Seq[ByteVector] = knownNetworks.map(_.p2shNetworkByte)
|
||||
|
||||
/** Uses the notation used in `bitcoin.conf` */
|
||||
def fromString(string: String): Option[NetworkParameters] = string match {
|
||||
case "mainnet" => Some(MainNet)
|
||||
case "testnet" => Some(TestNet3)
|
||||
case "regtest" => Some(RegTest)
|
||||
case _: String => None
|
||||
}
|
||||
|
||||
def bytesToNetwork: Map[ByteVector, NetworkParameters] = Map(
|
||||
MainNet.p2shNetworkByte -> MainNet,
|
||||
MainNet.p2pkhNetworkByte -> MainNet,
|
||||
|
|
|
@ -26,17 +26,20 @@ import org.slf4j.Logger
|
|||
|
||||
import scala.concurrent._
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
||||
import akka.stream.StreamTcpException
|
||||
|
||||
class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
||||
|
||||
implicit val system: ActorSystem = ActorSystem("EclairRpcClient")
|
||||
implicit val system: ActorSystem =
|
||||
ActorSystem("EclairRpcClient", BitcoindRpcTestUtil.AKKA_CONFIG)
|
||||
implicit val m: ActorMaterializer = ActorMaterializer.create(system)
|
||||
implicit val ec: ExecutionContext = m.executionContext
|
||||
implicit val bitcoinNp: RegTest.type = EclairRpcTestUtil.network
|
||||
|
||||
val logger: Logger = BitcoinSLogger.logger
|
||||
|
||||
val bitcoindRpcClientF: Future[BitcoindRpcClient] = {
|
||||
lazy val bitcoindRpcClientF: Future[BitcoindRpcClient] = {
|
||||
val cliF = EclairRpcTestUtil.startedBitcoindRpcClient()
|
||||
// make sure we have enough money open channels
|
||||
//not async safe
|
||||
|
@ -45,7 +48,7 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
|||
blocksF.flatMap(_ => cliF)
|
||||
}
|
||||
|
||||
val eclairNodesF: Future[EclairNodes4] = {
|
||||
lazy val eclairNodesF: Future[EclairNodes4] = {
|
||||
bitcoindRpcClientF.flatMap { bitcoindRpcClient =>
|
||||
val nodesF = EclairRpcTestUtil.createNodeLink(bitcoindRpcClient)
|
||||
|
||||
|
@ -110,6 +113,28 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
|||
|
||||
behavior of "RpcClient"
|
||||
|
||||
it should "be able to start and shutdown a node" in {
|
||||
for {
|
||||
bitcoind <- EclairRpcTestUtil.startedBitcoindRpcClient()
|
||||
eclair <- {
|
||||
val server = EclairRpcTestUtil.eclairInstance(bitcoind)
|
||||
val eclair = new EclairRpcClient(server)
|
||||
eclair.start().map(_ => eclair)
|
||||
}
|
||||
|
||||
_ <- eclair.getInfo
|
||||
|
||||
_ = EclairRpcTestUtil.shutdown(eclair)
|
||||
_ <- BitcoindRpcTestUtil.stopServer(bitcoind)
|
||||
|
||||
_ <- eclair.getInfo
|
||||
.map(_ => fail("Got info from a closed node!"))
|
||||
.recover {
|
||||
case _: StreamTcpException => ()
|
||||
}
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
it should "be able to open and close a channel" in {
|
||||
|
||||
val changeAddrF = bitcoindRpcClientF.flatMap(_.getNewAddress)
|
||||
|
@ -202,7 +227,10 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
val badCredentialsF = goodCredentialsF.map { good =>
|
||||
EclairAuthCredentials("bad_password", good.bitcoinAuthOpt, good.port)
|
||||
EclairAuthCredentials("bad_password",
|
||||
good.bitcoinAuthOpt,
|
||||
rpcPort = good.rpcPort,
|
||||
bitcoindRpcUri = good.bitcoindRpcUri)
|
||||
}
|
||||
|
||||
val badInstanceF = badCredentialsF.flatMap { badCredentials =>
|
||||
|
@ -714,7 +742,7 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
|||
|
||||
}
|
||||
|
||||
it must "receive gossip messages about channel updates for nodes we do not have a direct channel with" ignore {
|
||||
it must "receive gossip messages about channel updates for nodes we do not have a direct channel with" ignore {
|
||||
//make sure we see payments outside of our immediate peers
|
||||
//this is important because these gossip messages contain
|
||||
//information about channel fees, so we need to get updates
|
||||
|
|
|
@ -6,15 +6,16 @@ import org.bitcoins.testkit.eclair.rpc.EclairRpcTestUtil
|
|||
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
||||
import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll}
|
||||
import org.slf4j.LoggerFactory
|
||||
import akka.stream.StreamTcpException
|
||||
|
||||
class EclairRpcTestUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(getClass)
|
||||
|
||||
private implicit val actorSystem: ActorSystem =
|
||||
ActorSystem.create("EclairRpcTestUtilTest")
|
||||
ActorSystem("EclairRpcTestUtilTest", BitcoindRpcTestUtil.AKKA_CONFIG)
|
||||
|
||||
private val bitcoindRpcF = {
|
||||
private lazy val bitcoindRpcF = {
|
||||
val cliF = EclairRpcTestUtil.startedBitcoindRpcClient()
|
||||
val blocksF = cliF.flatMap(_.generate(200))
|
||||
blocksF.flatMap(_ => cliF)
|
||||
|
@ -30,6 +31,17 @@ class EclairRpcTestUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
|||
|
||||
behavior of "EclairRpcTestUtilTest"
|
||||
|
||||
it must "spawn a V16 bitcoind instance" in {
|
||||
for {
|
||||
bitcoind <- EclairRpcTestUtil.startedBitcoindRpcClient()
|
||||
_ <- bitcoind.getNetworkInfo
|
||||
_ <- BitcoindRpcTestUtil.stopServer(bitcoind)
|
||||
_ <- bitcoind.getNetworkInfo
|
||||
.map(_ => fail("got info from stopped bitcoind!"))
|
||||
.recover { case _: StreamTcpException => }
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
it must "spawn four nodes and create a channel link between them" in {
|
||||
val nodes4F = bitcoindRpcF.flatMap { bitcoindRpc =>
|
||||
val nodes = EclairRpcTestUtil.createNodeLink(bitcoindRpc)
|
||||
|
|
|
@ -137,7 +137,6 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
}
|
||||
|
||||
override def connect(uri: NodeUri): Future[String] = {
|
||||
logger.info(s"Connecting to $uri")
|
||||
eclairCall[String]("connect", List(JsString(uri.toString)))
|
||||
}
|
||||
|
||||
|
@ -583,7 +582,7 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
val p = Process(
|
||||
s"java -jar -Declair.datadir=${instance.authCredentials.datadir.get} $pathToEclairJar &")
|
||||
val result = p.run()
|
||||
logger.info(
|
||||
logger.debug(
|
||||
s"Starting eclair with datadir ${instance.authCredentials.datadir.get}")
|
||||
|
||||
process = Some(result)
|
||||
|
@ -605,8 +604,10 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
val p = Promise[Boolean]()
|
||||
|
||||
getInfo.onComplete {
|
||||
case Success(_) => p.success(true)
|
||||
case Failure(_) => p.success(false)
|
||||
case Success(_) =>
|
||||
p.success(true)
|
||||
case Failure(exc) =>
|
||||
p.success(false)
|
||||
}
|
||||
|
||||
p.future
|
||||
|
|
|
@ -5,6 +5,7 @@ import java.io.File
|
|||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import org.bitcoins.core.config.{MainNet, RegTest, TestNet3}
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials
|
||||
import java.net.URI
|
||||
|
||||
sealed trait EclairAuthCredentials {
|
||||
|
||||
|
@ -24,20 +25,19 @@ sealed trait EclairAuthCredentials {
|
|||
}
|
||||
|
||||
/** `rpcport` field in our `bitcoin.conf` file */
|
||||
def bitcoinRpcPort: Option[Int] = {
|
||||
bitcoinAuthOpt.map(_.rpcPort)
|
||||
}
|
||||
def bitcoindRpcUri: URI
|
||||
|
||||
/** `eclair.api.password` field in our `eclair.conf` file */
|
||||
def password: String
|
||||
|
||||
/** The port for eclair's rpc client */
|
||||
def port: Int
|
||||
def rpcPort: Int
|
||||
|
||||
def copyWithDatadir(datadir: File): EclairAuthCredentials = {
|
||||
EclairAuthCredentials(password = password,
|
||||
bitcoinAuthOpt = bitcoinAuthOpt,
|
||||
port = port,
|
||||
rpcPort = rpcPort,
|
||||
bitcoindRpcUri = bitcoindRpcUri,
|
||||
datadir = Some(datadir))
|
||||
}
|
||||
}
|
||||
|
@ -53,23 +53,22 @@ object EclairAuthCredentials {
|
|||
private case class AuthCredentialsImpl(
|
||||
password: String,
|
||||
bitcoinAuthOpt: Option[BitcoindAuthCredentials],
|
||||
port: Int,
|
||||
rpcPort: Int,
|
||||
bitcoindRpcUri: URI,
|
||||
datadir: Option[File])
|
||||
extends EclairAuthCredentials
|
||||
|
||||
def apply(
|
||||
password: String,
|
||||
bitcoinAuthOpt: Option[BitcoindAuthCredentials],
|
||||
port: Int): EclairAuthCredentials = {
|
||||
EclairAuthCredentials(password, bitcoinAuthOpt, port, None)
|
||||
}
|
||||
|
||||
def apply(
|
||||
password: String,
|
||||
bitcoinAuthOpt: Option[BitcoindAuthCredentials],
|
||||
port: Int,
|
||||
datadir: Option[File]): EclairAuthCredentials = {
|
||||
AuthCredentialsImpl(password, bitcoinAuthOpt, port, datadir)
|
||||
rpcPort: Int,
|
||||
bitcoindRpcUri: URI,
|
||||
datadir: Option[File] = None): EclairAuthCredentials = {
|
||||
AuthCredentialsImpl(password,
|
||||
bitcoinAuthOpt,
|
||||
rpcPort,
|
||||
bitcoindRpcUri,
|
||||
datadir)
|
||||
}
|
||||
|
||||
def fromDatadir(datadir: File): EclairAuthCredentials = {
|
||||
|
@ -104,19 +103,24 @@ object EclairAuthCredentials {
|
|||
"eclair.bitcoind.rpcport",
|
||||
defaultBitcoindPort)
|
||||
|
||||
val bitcoindRpcHost =
|
||||
ConfigUtil.getStringOrElse(config, "eclair.bitcoind.host", "localhost")
|
||||
|
||||
val bitcoindUri = new URI(s"http://$bitcoindRpcHost:$bitcoindRpcPort")
|
||||
|
||||
//does eclair not have a username field??
|
||||
val password = config.getString("eclair.api.password")
|
||||
val eclairRpcPort = ConfigUtil.getIntOrElse(config, "eclair.api.port", 8080)
|
||||
|
||||
val bitcoindAuth = {
|
||||
BitcoindAuthCredentials(username = bitcoindUsername,
|
||||
password = bitcoindPassword,
|
||||
rpcPort = bitcoindRpcPort)
|
||||
BitcoindAuthCredentials.PasswordBased(username = bitcoindUsername,
|
||||
password = bitcoindPassword)
|
||||
}
|
||||
|
||||
EclairAuthCredentials(password = password,
|
||||
bitcoinAuthOpt = Some(bitcoindAuth),
|
||||
port = eclairRpcPort,
|
||||
rpcPort = eclairRpcPort,
|
||||
bitcoindRpcUri = bitcoindUri,
|
||||
datadir = datadir)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.bitcoins.testkit.rpc.{BitcoindRpcTestUtil, TestRpcUtil}
|
|||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials
|
||||
|
||||
/**
|
||||
* @define nodeLinkDoc
|
||||
|
@ -90,15 +91,9 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
port: Int = RpcUtil.randomPort,
|
||||
rpcPort: Int = RpcUtil.randomPort,
|
||||
zmqPort: Int = RpcUtil.randomPort): BitcoindInstance = {
|
||||
val uri = new URI("http://localhost:" + port)
|
||||
val rpcUri = new URI("http://localhost:" + rpcPort)
|
||||
val auth = BitcoindRpcTestUtil.authCredentials(uri, rpcUri, zmqPort, false)
|
||||
|
||||
BitcoindInstance(network = network,
|
||||
uri = uri,
|
||||
rpcUri = rpcUri,
|
||||
authCredentials = auth,
|
||||
zmqConfig = ZmqConfig.fromPort(zmqPort))
|
||||
BitcoindRpcTestUtil.instance(port = port,
|
||||
rpcPort = rpcPort,
|
||||
zmqPort = zmqPort)
|
||||
}
|
||||
|
||||
//cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153
|
||||
|
@ -113,9 +108,13 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
"eclair.server.public-ips.1" -> "127.0.0.1",
|
||||
"eclair.server.binding-ip" -> "0.0.0.0",
|
||||
"eclair.server.port" -> port,
|
||||
"eclair.bitcoind.rpcuser" -> bitcoindInstance.authCredentials.username,
|
||||
"eclair.bitcoind.rpcpassword" -> bitcoindInstance.authCredentials.password,
|
||||
"eclair.bitcoind.rpcport" -> bitcoindInstance.authCredentials.rpcPort,
|
||||
"eclair.bitcoind.rpcuser" -> bitcoindInstance.authCredentials
|
||||
.asInstanceOf[BitcoindAuthCredentials.PasswordBased]
|
||||
.username,
|
||||
"eclair.bitcoind.rpcpassword" -> bitcoindInstance.authCredentials
|
||||
.asInstanceOf[BitcoindAuthCredentials.PasswordBased]
|
||||
.password,
|
||||
"eclair.bitcoind.rpcport" -> bitcoindInstance.rpcUri.getPort,
|
||||
// newer versions of Eclair has removed this config setting, in favor of
|
||||
// the below it. All three are included here for good measure
|
||||
"eclair.bitcoind.zmq" -> bitcoindInstance.zmqConfig.rawTx.get.toString,
|
||||
|
@ -553,15 +552,13 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
def getBitcoindRpc(eclairRpcClient: EclairRpcClient)(
|
||||
implicit system: ActorSystem): BitcoindRpcClient = {
|
||||
val bitcoindRpc = {
|
||||
val eclairAuth = eclairRpcClient.instance.authCredentials
|
||||
val bitcoindRpcPort = eclairAuth.bitcoinRpcPort.get
|
||||
|
||||
val instance = eclairRpcClient.instance
|
||||
val auth = instance.authCredentials
|
||||
val bitcoindInstance = BitcoindInstance(
|
||||
network = eclairRpcClient.instance.network,
|
||||
network = instance.network,
|
||||
uri = new URI("http://localhost:18333"),
|
||||
rpcUri = new URI(s"http://localhost:${bitcoindRpcPort}"),
|
||||
authCredentials =
|
||||
eclairRpcClient.instance.authCredentials.bitcoinAuthOpt.get
|
||||
rpcUri = auth.bitcoindRpcUri,
|
||||
authCredentials = auth.bitcoinAuthOpt.get
|
||||
)
|
||||
new BitcoindRpcClient(bitcoindInstance)(system)
|
||||
}
|
||||
|
@ -574,6 +571,7 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
import system.dispatcher
|
||||
val bitcoindRpc = getBitcoindRpc(eclairRpcClient)
|
||||
|
||||
logger.debug(s"shutting down eclair")
|
||||
eclairRpcClient.stop()
|
||||
|
||||
bitcoindRpc.stop().map(_ => ())
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package org.bitcoins.testkit.rpc
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.net.URI
|
||||
import java.nio.file.Paths
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory}
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.crypto.{
|
||||
DoubleSha256Digest,
|
||||
|
@ -50,13 +48,17 @@ import scala.collection.mutable
|
|||
import scala.concurrent._
|
||||
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
||||
import scala.util._
|
||||
import org.bitcoins.rpc.config.BitcoindConfig
|
||||
import java.nio.file.Files
|
||||
import java.io.File
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import java.nio.file.Path
|
||||
|
||||
//noinspection AccessorLikeMethodIsEmptyParen
|
||||
trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
||||
import BitcoindRpcTestUtil.DEFAULT_LONG_DURATION
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
type RpcClientAccum =
|
||||
mutable.Builder[BitcoindRpcClient, Vector[BitcoindRpcClient]]
|
||||
|
||||
|
@ -76,7 +78,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
/**
|
||||
* Standard config used for testing purposes
|
||||
*/
|
||||
def standardConfig: Config = {
|
||||
def standardConfig: BitcoindConfig = {
|
||||
def newUri: URI = new URI(s"http://localhost:${RpcUtil.randomPort}")
|
||||
config(uri = newUri,
|
||||
rpcUri = newUri,
|
||||
|
@ -88,88 +90,78 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
uri: URI,
|
||||
rpcUri: URI,
|
||||
zmqPort: Int,
|
||||
pruneMode: Boolean): Config = {
|
||||
pruneMode: Boolean): BitcoindConfig = {
|
||||
val pass = randomDirName
|
||||
val username = "random_user_name"
|
||||
val values = Map[String, String](
|
||||
"rpcuser" -> username,
|
||||
"rpcpassword" -> pass,
|
||||
"rpcport" -> rpcUri.getPort.toString,
|
||||
"port" -> uri.getPort.toString,
|
||||
"daemon" -> "1",
|
||||
"server" -> "1",
|
||||
"debug" -> "1",
|
||||
"regtest" -> "1",
|
||||
"walletbroadcast" -> "1",
|
||||
"txindex" -> (if (pruneMode) "0" else "1"), // pruning and txindex are not compatible
|
||||
"zmqpubhashtx" -> s"tcp://127.0.0.1:$zmqPort",
|
||||
"zmqpubhashblock" -> s"tcp://127.0.0.1:$zmqPort",
|
||||
"zmqpubrawtx" -> s"tcp://127.0.0.1:$zmqPort",
|
||||
"zmqpubrawblock" -> s"tcp://127.0.0.1:$zmqPort",
|
||||
"prune" -> (if (pruneMode) "1" else "0")
|
||||
)
|
||||
|
||||
val javaMap = values.asJava
|
||||
ConfigFactory.parseMap(javaMap)
|
||||
val conf = s"""
|
||||
|regtest=1
|
||||
|daemon=1
|
||||
|server=1
|
||||
|
|
||||
|rpcuser=$username
|
||||
|rpcpassword=$pass
|
||||
|rpcport=${rpcUri.getPort}
|
||||
|port=${uri.getPort}
|
||||
|debug=1
|
||||
|walletbroadcast=1
|
||||
|txindex=${if (pruneMode) 0 else 1 /* pruning and txindex are not compatible */}
|
||||
|zmqpubhashtx=tcp://127.0.0.1:$zmqPort
|
||||
|zmqpubhashblock=tcp://127.0.0.1:$zmqPort
|
||||
|zmqpubrawtx=tcp://127.0.0.1:$zmqPort
|
||||
|zmqpubrawblock=tcp://127.0.0.1:$zmqPort
|
||||
|prune=${if (pruneMode) 1 else 0}
|
||||
""".stripMargin
|
||||
BitcoindConfig(conf)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the `config` object has a `datadir` string. Returns the written
|
||||
* file.
|
||||
* Writes the config to the data directory within it, it it doesn't
|
||||
* exist. Returns the written file. Assumes the config has a datadir.
|
||||
*/
|
||||
def writeConfigToFile(config: Config): File = {
|
||||
def writeConfigToFile(config: BitcoindConfig): Path = {
|
||||
|
||||
val confSet = config.entrySet.asScala
|
||||
val confStr =
|
||||
confSet
|
||||
.map(entry => {
|
||||
val key = entry.getKey
|
||||
val value = entry.getValue.unwrapped
|
||||
s"$key=$value"
|
||||
})
|
||||
.mkString("\n")
|
||||
val confStr = config.lines.mkString("\n")
|
||||
|
||||
val datadir = new File(config.getString("datadir"))
|
||||
datadir.mkdir()
|
||||
val datadir = config.datadir
|
||||
.getOrElse(
|
||||
throw new IllegalArgumentException(
|
||||
"Provided bitcoind config does not have datadir field!"))
|
||||
.toPath
|
||||
|
||||
val confFile = new java.io.File(datadir.getAbsolutePath + "/bitcoin.conf")
|
||||
confFile.createNewFile()
|
||||
val confFile = datadir.resolve("bitcoin.conf")
|
||||
|
||||
val pw = new PrintWriter(confFile)
|
||||
pw.write(confStr)
|
||||
pw.close()
|
||||
Files.createDirectories(datadir)
|
||||
if (!Files.exists(confFile)) {
|
||||
Files.write(confFile, confStr.getBytes)
|
||||
}
|
||||
|
||||
confFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a datadir and places the username/password combo
|
||||
* in the bitcoin.conf in the datadir
|
||||
* Creates a `bitcoind` config within the system temp
|
||||
* directory, writes the file and returns the written
|
||||
* file
|
||||
*/
|
||||
def authCredentials(
|
||||
def writtenConfig(
|
||||
uri: URI,
|
||||
rpcUri: URI,
|
||||
zmqPort: Int,
|
||||
pruneMode: Boolean): BitcoindAuthCredentials = {
|
||||
pruneMode: Boolean
|
||||
): Path = {
|
||||
val conf = config(uri, rpcUri, zmqPort, pruneMode)
|
||||
|
||||
val configWithDatadir =
|
||||
if (conf.hasPath("datadir")) {
|
||||
if (conf.datadir.isDefined) {
|
||||
conf
|
||||
} else {
|
||||
conf.withValue("datadir",
|
||||
ConfigValueFactory.fromAnyRef("/tmp/" + randomDirName))
|
||||
val tempDir = Paths.get(Properties.tmpDir, randomDirName)
|
||||
conf.withOption("datadir", tempDir.toString)
|
||||
}
|
||||
|
||||
val configFile = writeConfigToFile(configWithDatadir)
|
||||
|
||||
val username = configWithDatadir.getString("rpcuser")
|
||||
val pass = configWithDatadir.getString("rpcpassword")
|
||||
|
||||
BitcoindAuthCredentials(username,
|
||||
pass,
|
||||
rpcUri.getPort,
|
||||
configFile.getParentFile)
|
||||
val written = writeConfigToFile(configWithDatadir)
|
||||
logger.debug(s"Wrote conf to ${written}")
|
||||
written
|
||||
}
|
||||
|
||||
lazy val network: RegTest.type = RegTest
|
||||
|
@ -202,6 +194,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
case BitcoindVersion.Unknown => BitcoindInstance.DEFAULT_BITCOIND_LOCATION
|
||||
}
|
||||
|
||||
/** Creates a `bitcoind` instance within the user temporary directory */
|
||||
def instance(
|
||||
port: Int = RpcUtil.randomPort,
|
||||
rpcPort: Int = RpcUtil.randomPort,
|
||||
|
@ -210,7 +203,9 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
versionOpt: Option[BitcoindVersion] = None): BitcoindInstance = {
|
||||
val uri = new URI("http://localhost:" + port)
|
||||
val rpcUri = new URI("http://localhost:" + rpcPort)
|
||||
val auth = authCredentials(uri, rpcUri, zmqPort, pruneMode)
|
||||
val configFile = writtenConfig(uri, rpcUri, zmqPort, pruneMode)
|
||||
val conf = BitcoindConfig(configFile)
|
||||
val auth = BitcoindAuthCredentials.fromConfig(conf)
|
||||
val binary = versionOpt match {
|
||||
case Some(version) =>
|
||||
getBinary(version)
|
||||
|
@ -221,7 +216,8 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
rpcUri = rpcUri,
|
||||
authCredentials = auth,
|
||||
zmqConfig = ZmqConfig.fromPort(zmqPort),
|
||||
binary = binary)
|
||||
binary = binary,
|
||||
datadir = configFile.getParent.toFile())
|
||||
|
||||
instance
|
||||
}
|
||||
|
@ -266,8 +262,16 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
|
||||
val serverStops = servers.map { s =>
|
||||
val stopF = s.stop()
|
||||
deleteTmpDir(s.getDaemon.authCredentials.datadir)
|
||||
stopF
|
||||
deleteTmpDir(s.getDaemon.datadir)
|
||||
stopF.onComplete {
|
||||
case Failure(exception) =>
|
||||
logger.error(s"Could not shut down sever: $exception")
|
||||
case Success(_) =>
|
||||
}
|
||||
for {
|
||||
stop <- stopF
|
||||
_ <- RpcUtil.awaitConditionF(() => s.isStoppedF)
|
||||
} yield stop
|
||||
}
|
||||
Future.sequence(serverStops).map(_ => ())
|
||||
}
|
||||
|
@ -280,7 +284,19 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
stopServers(Vector(server))
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given temporary directory
|
||||
*
|
||||
* @throws IllegalArgumentException if the
|
||||
* given directory isn't in the user
|
||||
* temp dir location
|
||||
*/
|
||||
def deleteTmpDir(dir: File): Boolean = {
|
||||
val isTemp = dir.getPath startsWith Properties.tmpDir
|
||||
if (!isTemp) {
|
||||
throw new IllegalArgumentException(
|
||||
s"Directory $dir is not in the system temp dir location! You most likely didn't mean to delete this directory.")
|
||||
}
|
||||
if (!dir.isDirectory) {
|
||||
dir.delete()
|
||||
} else {
|
||||
|
@ -806,7 +822,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
implicit executionContext: ExecutionContext): Future[Unit] = {
|
||||
val stopsF = List(client1, client2).map { client =>
|
||||
client.stop().map { _ =>
|
||||
deleteTmpDir(client.getDaemon.authCredentials.datadir)
|
||||
deleteTmpDir(client.getDaemon.datadir)
|
||||
}
|
||||
}
|
||||
Future.sequence(stopsF).map(_ => ())
|
||||
|
@ -841,6 +857,10 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
|
|||
clientAccum: RpcClientAccum = Vector.newBuilder)(
|
||||
implicit system: ActorSystem): Future[BitcoindRpcClient] = {
|
||||
implicit val ec: ExecutionContextExecutor = system.dispatcher
|
||||
assert(
|
||||
instance.datadir.getPath().startsWith(Properties.tmpDir),
|
||||
s"${instance.datadir} is not in user temp dir! This could lead to bad things happening.")
|
||||
|
||||
//start the bitcoind instance so eclair can properly use it
|
||||
val rpc = new BitcoindRpcClient(instance)
|
||||
val startedF = rpc.start()
|
||||
|
|
Loading…
Add table
Reference in a new issue