Add support for Bitcoin Core 0.17 (#384)

This commit is contained in:
Torkel Rogstad 2019-03-26 18:41:05 +01:00 committed by Chris Stewart
parent 4d88354073
commit fb16d6b200
59 changed files with 5995 additions and 323 deletions

View File

@ -4,6 +4,6 @@ rpcuser=user
rpcpassword=password
zmqpubrawblock=tcp://127.0.0.1:29005
zmppubrawtx=tcp://127.0.0.1:29001
rpcport=18336
rpcport=18500
port=9000
daemon=1

View File

@ -2,21 +2,15 @@ package org.bitcoins.rpc
import java.io.{File, PrintWriter}
import java.nio.file.{Files, Path}
import akka.actor.ActorSystem
import akka.testkit.TestKit
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.testkit.rpc.{BitcoindRpcTestUtil, TestRpcUtil}
import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
import scala.io.Source
class BitcoindInstanceTest extends AsyncFlatSpec with BeforeAndAfterAll {
private implicit val actorSystem: ActorSystem = ActorSystem(
"BitcoindInstanceTest")
class BitcoindInstanceTest extends BitcoindRpcTest {
private val source =
Source.fromURL(getClass.getResource("/sample-bitcoin.conf"))
@ -34,27 +28,18 @@ class BitcoindInstanceTest extends AsyncFlatSpec with BeforeAndAfterAll {
pw.close()
}
override protected def afterAll(): Unit = {
TestKit.shutdownActorSystem(actorSystem)
}
behavior of "BitcoindInstance"
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)
BitcoindRpcTestUtil.startServers(Vector(client))
TestRpcUtil.awaitServer(client)
for {
_ <- client.start()
_ <- client.generate(101)
balance <- client.getBalance
_ <- {
assert(balance > Bitcoins(0))
client.stop()
}
_ <- Future.successful(TestRpcUtil.awaitServerShutdown(client))
} yield succeed
_ <- BitcoindRpcTestUtil.stopServers(Vector(client))
} yield assert(balance > Bitcoins(0))
}

View File

@ -2,21 +2,22 @@ package org.bitcoins.rpc
import java.io.File
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.testkit.rpc.{BitcoindRpcTestUtil, TestRpcUtil}
import org.scalatest.exceptions.TestFailedException
import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.common.RpcOpts.AddNodeArgument
import org.bitcoins.rpc.util.AsyncUtil.RpcRetryException
import org.bitcoins.rpc.util.{AsyncUtil, RpcUtil}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, Future}
import scala.util.Success
class TestRpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
class TestRpcUtilTest extends BitcoindRpcTest {
implicit val system = ActorSystem("RpcUtilTest_ActorSystem")
implicit val ec = system.dispatcher
private lazy val clientsF =
BitcoindRpcTestUtil.createNodeTriple(clientAccum = clientAccum)
private def trueLater(delay: Int = 1000): Future[Boolean] = Future {
Thread.sleep(delay)
@ -31,13 +32,13 @@ class TestRpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
private def boolLaterDoneAndTrue(
trueLater: Future[Boolean]): () => Future[Boolean] = { () =>
boolLaterDoneAnd(true, trueLater)
boolLaterDoneAnd(bool = true, trueLater)
}
behavior of "TestRpcUtil"
it should "complete immediately if condition is true" in {
TestRpcUtil
AsyncUtil
.retryUntilSatisfiedF(conditionF = () => Future.successful(true),
duration = 0.millis)
.map { _ =>
@ -46,58 +47,68 @@ class TestRpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
}
it should "fail if condition is false" in {
recoverToSucceededIf[TestFailedException] {
TestRpcUtil.retryUntilSatisfiedF(conditionF =
() => Future.successful(false),
duration = 0.millis)
recoverToSucceededIf[RpcRetryException] {
AsyncUtil.retryUntilSatisfiedF(
conditionF = () => Future.successful(false),
duration = 0.millis)
}
}
it should "succeed after a delay" in {
val boolLater = trueLater(delay = 250)
TestRpcUtil.retryUntilSatisfiedF(boolLaterDoneAndTrue(boolLater)).map { _ =>
AsyncUtil.retryUntilSatisfiedF(boolLaterDoneAndTrue(boolLater)).map { _ =>
succeed
}
}
it should "fail if there is a delay and duration is zero" in {
val boolLater = trueLater(delay = 250)
recoverToSucceededIf[TestFailedException] {
TestRpcUtil.retryUntilSatisfiedF(boolLaterDoneAndTrue(boolLater),
duration = 0.millis)
recoverToSucceededIf[RpcRetryException] {
AsyncUtil
.retryUntilSatisfiedF(boolLaterDoneAndTrue(boolLater),
duration = 0.millis)
.map(_ => succeed)
}
}
it should "succeed immediately if condition is true" in {
TestRpcUtil.awaitCondition(condition = () => true, 0.millis)
succeed
AsyncUtil
.awaitCondition(condition = () => true, 0.millis)
.map(_ => succeed)
}
it should "timeout if condition is false" in {
assertThrows[TestFailedException] {
TestRpcUtil.awaitCondition(condition = () => false, duration = 0.millis)
recoverToSucceededIf[RpcRetryException] {
AsyncUtil
.awaitCondition(condition = () => false, duration = 0.millis)
.map(_ => succeed)
}
}
it should "block for a delay and then succeed" in {
it should "wait for a delay and then succeed" in {
val boolLater = trueLater(delay = 250)
val before: Long = System.currentTimeMillis
TestRpcUtil.awaitConditionF(boolLaterDoneAndTrue(boolLater))
val after: Long = System.currentTimeMillis
assert(after - before >= 250)
AsyncUtil.awaitConditionF(boolLaterDoneAndTrue(boolLater)).flatMap { _ =>
val after: Long = System.currentTimeMillis
assert(after - before >= 250)
}
}
it should "timeout if there is a delay and duration is zero" in {
val boolLater = trueLater(delay = 250)
assertThrows[TestFailedException] {
TestRpcUtil.awaitConditionF(boolLaterDoneAndTrue(boolLater),
duration = 0.millis)
recoverToSucceededIf[RpcRetryException] {
AsyncUtil
.awaitConditionF(boolLaterDoneAndTrue(boolLater), duration = 0.millis)
.map(_ => succeed)
}
}
"BitcoindRpcUtil" should "create a temp bitcoin directory when creating a DaemonInstance, and then delete it" in {
val instance = BitcoindRpcTestUtil.instance(BitcoindRpcTestUtil.randomPort,
BitcoindRpcTestUtil.randomPort)
behavior of "BitcoindRpcUtil"
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
assert(dir.isDirectory)
assert(
@ -107,9 +118,6 @@ class TestRpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
}
it should "be able to create a single node, wait for it to start and then delete it" in {
implicit val m: ActorMaterializer = ActorMaterializer.create(system)
implicit val ec = m.executionContext
val instance = BitcoindRpcTestUtil.instance()
val client = new BitcoindRpcClient(instance)
val startedF = client.start()
@ -120,31 +128,91 @@ class TestRpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
}
}
it should "be able to create a connected node pair with 100 blocks and then delete them" in {
BitcoindRpcTestUtil.createNodePair().flatMap {
case (client1, client2) =>
assert(client1.getDaemon.authCredentials.datadir.isDirectory)
assert(client2.getDaemon.authCredentials.datadir.isDirectory)
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)
client1.getAddedNodeInfo(client2.getDaemon.uri).flatMap { nodes =>
assert(nodes.nonEmpty)
nodes <- client1.getAddedNodeInfo(client2.getDaemon.uri)
_ = assert(nodes.nonEmpty)
client1.getBlockCount.flatMap { count1 =>
assert(count1 == 100)
client2.getBlockCount.map { count2 =>
assert(count2 == 100)
BitcoindRpcTestUtil.deleteNodePair(client1, client2)
assert(!client1.getDaemon.authCredentials.datadir.exists)
assert(!client2.getDaemon.authCredentials.datadir.exists)
}
}
}
count1 <- client1.getBlockCount
count2 <- client2.getBlockCount
_ = assert(count1 > 100)
_ = assert(count2 > 100)
_ <- BitcoindRpcTestUtil.deleteNodePair(client1, client2)
} yield {
assert(!client1.getDaemon.authCredentials.datadir.exists)
assert(!client2.getDaemon.authCredentials.datadir.exists)
}
}
override def afterAll(): Unit = {
Await.result(system.terminate(), 10.seconds)
it should "be able to generate and sync blocks" in {
for {
(first, second, third) <- clientsF
address <- second.getNewAddress
txid <- first.sendToAddress(address, Bitcoins.one)
_ <- BitcoindRpcTestUtil.generateAndSync(Vector(first, second, third))
tx <- first.getTransaction(txid)
_ = assert(tx.confirmations > 0)
rawTx <- second.getRawTransaction(txid)
_ = assert(rawTx.confirmations.exists(_ > 0))
firstBlock <- first.getBestBlockHash
secondBlock <- second.getBestBlockHash
} yield assert(firstBlock == secondBlock)
}
it should "ble able to generate blocks with multiple clients and sync inbetween" in {
val blocksToGenerate = 10
for {
(first, second, third) <- clientsF
allClients = Vector(first, second, third)
heightPreGeneration <- first.getBlockCount
_ <- BitcoindRpcTestUtil.generateAllAndSync(allClients,
blocks = blocksToGenerate)
firstHash <- first.getBestBlockHash
secondHash <- second.getBestBlockHash
heightPostGeneration <- first.getBlockCount
} yield {
assert(firstHash == secondHash)
assert(
heightPostGeneration - heightPreGeneration == blocksToGenerate * allClients.length)
}
}
it should "be able to wait for disconnected nodes" in {
for {
(first, second) <- BitcoindRpcTestUtil.createUnconnectedNodePair(
clientAccum)
_ <- first.addNode(second.instance.uri, AddNodeArgument.Add)
_ <- BitcoindRpcTestUtil.awaitConnection(first, second)
peerInfo <- first.getPeerInfo
_ = assert(peerInfo.length == 1)
_ = assert(peerInfo.head.addnode)
_ = assert(peerInfo.head.networkInfo.addr == second.instance.uri)
_ <- first.disconnectNode(peerInfo.head.networkInfo.addr)
_ <- BitcoindRpcTestUtil.awaitDisconnected(first, second)
newPeerInfo <- first.getPeerInfo
} yield assert(newPeerInfo.isEmpty)
}
it should "be able to find outputs of previous transactions" in {
for {
(first, second, _) <- clientsF
address <- second.getNewAddress
txid <- first.sendToAddress(address, Bitcoins.one)
hashes <- BitcoindRpcTestUtil.generateAndSync(Vector(first, second))
vout <- BitcoindRpcTestUtil.findOutput(first,
txid,
Bitcoins.one,
Some(hashes.head))
tx <- first.getRawTransaction(txid, Some(hashes.head))
} yield {
assert(tx.vout(vout.toInt).value == Bitcoins.one)
}
}
}

View File

@ -0,0 +1,289 @@
package org.bitcoins.rpc.common
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.common.RpcOpts.{AddNodeArgument, AddressType}
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class BlockchainRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodePair(clientAccum = clientAccum)
lazy val pruneClientF: Future[BitcoindRpcClient] = clientsF.flatMap {
case (_, _) =>
val pruneClient =
new BitcoindRpcClient(BitcoindRpcTestUtil.instance(pruneMode = true))
clientAccum += pruneClient
for {
_ <- pruneClient.start()
_ <- pruneClient.generate(1000)
} yield pruneClient
}
behavior of "BlockchainRpc"
it should "be able to get the block count" in {
for {
(client, otherClient) <- clientsF
// kick off both futures at the same time to avoid
// one of them generating new blocks in between
clientCountF = client.getBlockCount
otherClientCountF = otherClient.getBlockCount
List(clientCount, otherClientCount) <- {
val countsF = List(clientCountF, otherClientCountF)
Future.sequence(countsF)
}
} yield {
assert(clientCount >= 0)
assert(clientCount == otherClientCount)
}
}
it should "be able to get the first block" in {
for {
(client, _) <- clientsF
block <- BitcoindRpcTestUtil.getFirstBlock(client)
} yield {
assert(block.tx.nonEmpty)
assert(block.height == 1)
}
}
it should "be able to prune the blockchain" in {
for {
pruneClient <- pruneClientF
count <- pruneClient.getBlockCount
pruned <- pruneClient.pruneBlockChain(count)
} yield {
assert(pruned > 0)
}
}
it should "be able to get blockchain info" in {
for {
(client, _) <- clientsF
info <- client.getBlockChainInfo
bestHash <- client.getBestBlockHash
} yield {
assert(info.chain == "regtest")
assert(info.softforks.length >= 3)
assert(info.bip9_softforks.keySet.size >= 2)
assert(info.bestblockhash == bestHash)
}
}
it should "be able to invalidate a block" in {
for {
(client, otherClient) <- clientsF
address <- otherClient.getNewAddress(addressType = AddressType.P2SHSegwit)
txid <- BitcoindRpcTestUtil
.fundMemPoolTransaction(client, address, Bitcoins(1))
blocks <- client.generate(1)
mostRecentBlock <- client.getBlock(blocks.head)
_ <- client.invalidateBlock(blocks.head)
mempool <- client.getRawMemPool
count1 <- client.getBlockCount
count2 <- otherClient.getBlockCount
_ <- client.generate(2) // Ensure client and otherClient have the same blockchain
} yield {
assert(mostRecentBlock.tx.contains(txid))
assert(mempool.contains(txid))
assert(count1 == count2 - 1)
}
}
it should "be able to get block hash by height" in {
for {
(client, _) <- clientsF
blocks <- client.generate(2)
count <- client.getBlockCount
hash <- client.getBlockHash(count)
prevhash <- client.getBlockHash(count - 1)
} yield {
assert(blocks(1) == hash)
assert(blocks(0) == prevhash)
}
}
it should "be able to mark a block as precious" in {
for {
(freshClient, otherFreshClient) <- BitcoindRpcTestUtil.createNodePair(
clientAccum)
_ <- freshClient.disconnectNode(otherFreshClient.getDaemon.uri)
_ <- BitcoindRpcTestUtil.awaitDisconnected(freshClient, otherFreshClient)
blocks1 <- freshClient.generate(1)
blocks2 <- otherFreshClient.generate(1)
bestHash1 <- freshClient.getBestBlockHash
_ = assert(bestHash1 == blocks1.head)
bestHash2 <- otherFreshClient.getBestBlockHash
_ = assert(bestHash2 == blocks2.head)
_ <- freshClient
.addNode(otherFreshClient.getDaemon.uri, AddNodeArgument.OneTry)
_ <- AsyncUtil.retryUntilSatisfiedF(() =>
BitcoindRpcTestUtil.hasSeenBlock(otherFreshClient, bestHash1))
_ <- otherFreshClient.preciousBlock(bestHash1)
newBestHash <- otherFreshClient.getBestBlockHash
} yield assert(newBestHash == bestHash1)
}
it should "be able to get tx out proof and verify it" in {
for {
(client, _) <- clientsF
block <- BitcoindRpcTestUtil.getFirstBlock(client)
merkle <- client.getTxOutProof(Vector(block.tx.head.txid))
txids <- client.verifyTxOutProof(merkle)
} yield {
assert(merkle.transactionCount == UInt32(1))
assert(merkle.hashes.length == 1)
assert(merkle.hashes.head.flip == block.tx.head.txid)
assert(block.tx.head.txid == txids.head)
}
}
it should "be able to rescan the blockchain" in {
for {
(client, _) <- clientsF
result <- client.rescanBlockChain()
count <- client.getBlockCount
} yield {
assert(result.start_height == 0)
assert(count == result.stop_height)
}
}
it should "be able to get the chain tx stats" in {
for {
(client, _) <- clientsF
stats <- client.getChainTxStats
} yield {
assert(stats.txcount > 0)
assert(stats.window_block_count > 0)
}
}
it should "be able to get a raw block" in {
for {
(client, _) <- clientsF
blocks <- client.generate(1)
block <- client.getBlockRaw(blocks.head)
blockHeader <- client.getBlockHeaderRaw(blocks.head)
} yield assert(block.blockHeader == blockHeader)
}
it should "be able to get a block" in {
for {
(client, _) <- clientsF
blocks <- client.generate(1)
block <- client.getBlock(blocks.head)
} yield {
assert(block.hash == blocks(0))
assert(block.confirmations == 1)
assert(block.size > 0)
assert(block.weight > 0)
assert(block.height > 0)
assert(block.difficulty > 0)
}
}
it should "be able to get a transaction" in {
for {
(client, _) <- clientsF
block <- BitcoindRpcTestUtil.getFirstBlock(client)
tx <- client.getTransaction(block.tx.head.txid)
count <- client.getBlockCount
} yield {
assert(tx.txid == block.tx.head.txid)
assert(tx.amount == Bitcoins(50))
assert(tx.blockindex.get == 0)
assert(tx.details.head.category == "generate")
assert(tx.generated.get)
assert(tx.confirmations == count)
}
}
it should "be able to get a block with verbose transactions" in {
for {
(client, _) <- clientsF
blocks <- client.generate(2)
block <- client.getBlockWithTransactions(blocks(1))
} yield {
assert(block.hash == blocks(1))
assert(block.tx.length == 1)
val tx = block.tx.head
assert(tx.vout.head.n == 0)
}
}
it should "be able to get the chain tips" in {
for {
(client, _) <- clientsF
_ <- client.getChainTips
} yield succeed
}
it should "be able to get the best block hash" in {
for {
(client, _) <- clientsF
_ <- client.getBestBlockHash
} yield succeed
}
it should "be able to list all blocks since a given block" in {
for {
(client, _) <- clientsF
blocks <- client.generate(3)
list <- client.listSinceBlock(blocks(0))
} yield {
assert(list.transactions.length >= 2)
assert(list.transactions.exists(_.blockhash.contains(blocks(1))))
assert(list.transactions.exists(_.blockhash.contains(blocks(2))))
}
}
it should "be able to verify the chain" in {
for {
(client, _) <- clientsF
valid <- client.verifyChain(blocks = 0)
} yield assert(valid)
}
it should "be able to get the tx outset info" in {
for {
(client, _) <- clientsF
info <- client.getTxOutSetInfo
count <- client.getBlockCount
hash <- client.getBestBlockHash
} yield {
assert(info.height == count)
assert(info.bestblock == hash)
}
}
it should "be able to list transactions in a given range" in { // Assumes 30 transactions
for {
(client, _) <- clientsF
list1 <- client.listTransactions()
list2 <- client.listTransactions(count = 20)
list3 <- client.listTransactions(count = 20, skip = 10)
} yield {
assert(list2.takeRight(10) == list1)
assert(list2.splitAt(10)._1 == list3.takeRight(10))
}
}
}

View File

@ -0,0 +1,204 @@
package org.bitcoins.rpc.common
import java.io.File
import java.nio.file.Files
import com.typesafe.config.ConfigValueFactory
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script.ScriptSignature
import org.bitcoins.core.protocol.transaction.{
TransactionInput,
TransactionOutPoint
}
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class MempoolRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodePair(clientAccum = clientAccum)
lazy val clientWithoutBroadcastF: Future[BitcoindRpcClient] =
clientsF.flatMap {
case (client, otherClient) =>
val defaultConfig = BitcoindRpcTestUtil.standardConfig
val datadirValue = {
val tempDirPrefix = null // because java APIs are bad
val tempdirPath = Files.createTempDirectory(tempDirPrefix).toString
ConfigValueFactory.fromAnyRef(tempdirPath)
}
// 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)
val _ = BitcoindRpcTestUtil.writeConfigToFile(configNoBroadcast)
val instanceWithoutBroadcast =
BitcoindInstance.fromConfig(configNoBroadcast)
val clientWithoutBroadcast =
new BitcoindRpcClient(instanceWithoutBroadcast)
clientAccum += clientWithoutBroadcast
val pairs = Vector(client -> clientWithoutBroadcast,
otherClient -> clientWithoutBroadcast)
for {
_ <- clientWithoutBroadcast.start()
_ <- BitcoindRpcTestUtil.connectPairs(pairs)
_ <- BitcoindRpcTestUtil.syncPairs(pairs)
_ <- BitcoindRpcTestUtil.generateAndSync(
Vector(clientWithoutBroadcast, client, otherClient),
blocks = 200)
} yield clientWithoutBroadcast
}
behavior of "MempoolRpc"
it should "be able to find a transaction sent to the mem pool" in {
for {
(client, otherClient) <- clientsF
transaction <- BitcoindRpcTestUtil.sendCoinbaseTransaction(client,
otherClient)
mempool <- client.getRawMemPool
} yield {
assert(mempool.length == 1)
assert(mempool.head == transaction.txid)
}
}
it should "be able to find a verbose transaction in the mem pool" in {
for {
(client, otherClient) <- clientsF
transaction <- BitcoindRpcTestUtil.sendCoinbaseTransaction(client,
otherClient)
mempool <- client.getRawMemPoolWithTransactions
} yield {
val txid = mempool.keySet.head
assert(txid == transaction.txid)
assert(mempool(txid).size > 0)
}
}
it should "be able to find a mem pool entry" in {
for {
(client, otherClient) <- clientsF
transaction <- BitcoindRpcTestUtil.sendCoinbaseTransaction(client,
otherClient)
_ <- client.getMemPoolEntry(transaction.txid)
} yield succeed
}
it should "be able to get mem pool info" in {
for {
(client, otherClient) <- clientsF
_ <- client.generate(1)
info <- client.getMemPoolInfo
_ <- BitcoindRpcTestUtil
.sendCoinbaseTransaction(client, otherClient)
newInfo <- client.getMemPoolInfo
} yield {
assert(info.size == 0)
assert(newInfo.size == 1)
}
}
it should "be able to prioritise a mem pool transaction" in {
for {
(client, otherClient) <- clientsF
address <- otherClient.getNewAddress
txid <- BitcoindRpcTestUtil
.fundMemPoolTransaction(client, address, Bitcoins(3.2))
entry <- client.getMemPoolEntry(txid)
tt <- client.prioritiseTransaction(txid, Bitcoins(1).satoshis)
newEntry <- client.getMemPoolEntry(txid)
} yield {
assert(entry.fee == entry.modifiedfee)
assert(tt)
assert(newEntry.fee == entry.fee)
assert(newEntry.modifiedfee == newEntry.fee + Bitcoins(1))
}
}
it should "be able to find mem pool ancestors and descendants" in {
for {
(client, _) <- clientsF
_ <- client.generate(1)
address1 <- client.getNewAddress
txid1 <- BitcoindRpcTestUtil.fundMemPoolTransaction(client,
address1,
Bitcoins(2))
mempool <- client.getRawMemPool
address2 <- client.getNewAddress
createdTx <- {
val input: TransactionInput =
TransactionInput(TransactionOutPoint(txid1.flip, UInt32.zero),
ScriptSignature.empty,
UInt32.max - UInt32.one)
client
.createRawTransaction(Vector(input), Map(address2 -> Bitcoins.one))
}
signedTx <- BitcoindRpcTestUtil.signRawTransaction(client, createdTx)
txid2 <- client.sendRawTransaction(signedTx.hex, allowHighFees = true)
descendantsTxid1 <- client.getMemPoolDescendants(txid1)
verboseDescendantsTxid1 <- client.getMemPoolDescendantsVerbose(txid1)
_ = {
assert(descendantsTxid1.head == txid2)
val (txid, mempoolresults) = verboseDescendantsTxid1.head
assert(txid == txid2)
assert(mempoolresults.ancestorcount == 2)
}
ancestorsTxid2 <- client.getMemPoolAncestors(txid2)
verboseAncestorsTxid2 <- client.getMemPoolAncestorsVerbose(txid2)
_ = {
assert(ancestorsTxid2.head == txid1)
val (txid, mempoolreults) = verboseAncestorsTxid2.head
assert(txid == txid1)
assert(mempoolreults.descendantcount == 2)
}
} yield {
assert(mempool.head == txid1)
assert(signedTx.complete)
}
}
it should "be able to abandon a transaction" in {
for {
(_, otherClient) <- clientsF
clientWithoutBroadcast <- clientWithoutBroadcastF
recipient <- otherClient.getNewAddress
txid <- clientWithoutBroadcast.sendToAddress(recipient, Bitcoins(1))
_ <- clientWithoutBroadcast.abandonTransaction(txid)
maybeAbandoned <- clientWithoutBroadcast.getTransaction(txid)
} yield assert(maybeAbandoned.details.head.abandoned.contains(true))
}
it should "be able to save the mem pool to disk" in {
for {
(client, _) <- clientsF
regTest = {
val regTest =
new File(client.getDaemon.authCredentials.datadir + "/regtest")
assert(regTest.isDirectory)
assert(!regTest.list().contains("mempool.dat"))
regTest
}
_ <- client.saveMemPool()
} yield assert(regTest.list().contains("mempool.dat"))
}
}

View File

@ -0,0 +1,45 @@
package org.bitcoins.rpc.common
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class MessageRpcTest extends BitcoindRpcTest {
val clientF: Future[BitcoindRpcClient] =
BitcoindRpcTestUtil.startedBitcoindRpcClient().map { client =>
clientAccum += client
client
}
behavior of "MessageRpc"
it should "be able to sign a message and verify that signature" in {
val message = "Never gonna give you up\nNever gonna let you down\n..."
for {
client <- clientF
address <- client.getNewAddress(addressType = AddressType.Legacy)
signature <- client.signMessage(address.asInstanceOf[P2PKHAddress],
message)
validity <- client
.verifyMessage(address.asInstanceOf[P2PKHAddress], signature, message)
} yield assert(validity)
}
it should "be able to sign a message with a private key and verify that signature" in {
val message = "Never gonna give you up\nNever gonna let you down\n..."
val privKey = ECPrivateKey.freshPrivateKey
val address = P2PKHAddress(privKey.publicKey, networkParam)
for {
client <- clientF
signature <- client.signMessageWithPrivKey(privKey, message)
validity <- client.verifyMessage(address, signature, message)
} yield assert(validity)
}
}

View File

@ -0,0 +1,94 @@
package org.bitcoins.rpc.common
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class MiningRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodePair(clientAccum = clientAccum)
behavior of "MiningRpc"
it should "be able to get a block template" in {
clientsF.flatMap {
case (client, _) =>
val getBlockF = client.getBlockTemplate()
getBlockF
.recover {
// getblocktemplate is having a bad time on regtest
// https://github.com/bitcoin/bitcoin/issues/11379
case err: Throwable
if err.getMessage
.contains("-9") =>
succeed
case other: Throwable => throw other
}
.map(_ => succeed)
}
}
it should "be able to generate blocks" in {
for {
(client, _) <- clientsF
blocks <- client.generate(3)
} yield assert(blocks.length == 3)
}
it should "be able to get the mining info" in {
for {
(client, _) <- clientsF
info <- client.getMiningInfo
} yield assert(info.chain == "regtest")
}
it should "be able to generate blocks to an address" in {
for {
(client, otherClient) <- clientsF
address <- otherClient.getNewAddress
blocks <- client.generateToAddress(3, address)
foundBlocks <- {
val hashFuts = blocks.map(client.getBlockWithTransactions)
Future.sequence(hashFuts)
}
} yield {
assert(blocks.length == 3)
assert(blocks.length == 3)
foundBlocks.foreach { found =>
assert(
found.tx.head.vout.head.scriptPubKey.addresses.get.head == address)
}
succeed
}
}
it should "be able to generate blocks and then get their serialized headers" in {
for {
(client, _) <- clientsF
blocks <- client.generate(2)
header <- client.getBlockHeaderRaw(blocks(1))
} yield assert(header.previousBlockHashBE == blocks(0))
}
it should "be able to generate blocks and then get their headers" in {
for {
(client, _) <- clientsF
blocks <- client.generate(2)
firstHeader <- client.getBlockHeader(blocks(0))
secondHeader <- client.getBlockHeader(blocks(1))
} yield {
assert(firstHeader.nextblockhash.contains(blocks(1)))
assert(secondHeader.previousblockhash.contains(blocks(0)))
assert(secondHeader.nextblockhash.isEmpty)
}
}
it should "be able to get the network hash per sec" in {
for {
(client, _) <- clientsF
hps <- client.getNetworkHashPS()
} yield assert(hps > 0)
}
}

View File

@ -0,0 +1,47 @@
package org.bitcoins.rpc.common
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class MultisigRpcTest extends BitcoindRpcTest {
lazy val clientF: Future[BitcoindRpcClient] =
BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum)
behavior of "MultisigRpc"
it should "be able to create a multi sig address" in {
val ecPrivKey1 = ECPrivateKey.freshPrivateKey
val ecPrivKey2 = ECPrivateKey.freshPrivateKey
val pubKey1 = ecPrivKey1.publicKey
val pubKey2 = ecPrivKey2.publicKey
for {
client <- clientF
_ <- client.createMultiSig(2, Vector(pubKey1, pubKey2))
} yield succeed
}
it should "be able to add a multi sig address to the wallet" in {
val ecPrivKey1 = ECPrivateKey.freshPrivateKey
val pubKey1 = ecPrivKey1.publicKey
for {
client <- clientF
address <- client.getNewAddress(addressType = AddressType.Legacy)
_ <- {
val pubkey = Left(pubKey1)
val p2pkh = Right(address.asInstanceOf[P2PKHAddress])
client
.addMultiSigAddress(2, Vector(pubkey, p2pkh))
}
} yield succeed
}
}

View File

@ -0,0 +1,78 @@
package org.bitcoins.rpc.common
import org.bitcoins.core.number.UInt32
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class NodeRpcTest extends BitcoindRpcTest {
lazy val clientF: Future[BitcoindRpcClient] =
BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum)
behavior of "NodeRpc"
it should "be able to abort a rescan of the blockchain" in {
clientF.flatMap { client =>
// generate some extra blocks so rescan isn't too quick
client.generate(3000).flatMap { _ =>
val rescanFailedF =
recoverToSucceededIf[RuntimeException](client.rescanBlockChain())
client.abortRescan().flatMap { _ =>
rescanFailedF
}
}
}
}
it should "be able to ping" in {
for {
client <- clientF
_ <- client.ping()
} yield succeed
}
it should "be able to get and set the logging configuration" in {
for {
client <- clientF
info <- client.logging
infoNoQt <- client.logging(exclude = Vector("qt"))
} yield {
info.keySet.foreach(category => assert(info(category)))
assert(!infoNoQt("qt"))
}
}
it should "be able to get the memory info" in {
for {
client <- clientF
info <- client.getMemoryInfo
} yield {
assert(info.locked.used > 0)
assert(info.locked.free > 0)
assert(info.locked.total > 0)
assert(info.locked.locked > 0)
assert(info.locked.chunks_used > 0)
}
}
it should "be able to get the client's uptime" in {
for {
client <- clientF
time <- client.uptime
} yield assert(time > UInt32(0))
}
it should "be able to get help from bitcoind" in {
for {
client <- clientF
genHelp <- client.help()
helpHelp <- client.help("help")
} yield {
assert(!genHelp.isEmpty)
assert(genHelp != helpHelp)
assert(!helpHelp.isEmpty)
}
}
}

View File

@ -0,0 +1,206 @@
package org.bitcoins.rpc.common
import java.net.URI
import org.bitcoins.core.number.UInt32
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.common.RpcOpts.{AddNodeArgument, SetBanCommand}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class P2PRpcTest extends BitcoindRpcTest {
lazy val clientF: Future[BitcoindRpcClient] =
BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum)
lazy val clientPairF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodePair(clientAccum)
behavior of "P2PRpcTest"
it should "be able to get peer info" in {
for {
(freshClient, otherFreshClient) <- clientPairF
infoList <- freshClient.getPeerInfo
} yield {
assert(infoList.length >= 0)
val info = infoList.head
assert(info.addnode)
assert(info.networkInfo.addr == otherFreshClient.getDaemon.uri)
}
}
it should "be able to get the added node info" in {
for {
(freshClient, otherFreshClient) <- clientPairF
info <- freshClient.getAddedNodeInfo
} yield {
assert(info.length == 1)
assert(info.head.addednode == otherFreshClient.getDaemon.uri)
assert(info.head.connected.contains(true))
}
}
it should "be able to get the network info" in {
for {
(freshClient, _) <- clientPairF
info <- freshClient.getNetworkInfo
} yield {
assert(info.networkactive)
assert(info.localrelay)
assert(info.connections == 1)
}
}
it should "be able to get network statistics" in {
for {
(connectedClient, _) <- clientPairF
stats <- connectedClient.getNetTotals
} yield {
assert(stats.timemillis.toBigInt > 0)
assert(stats.totalbytesrecv > 0)
assert(stats.totalbytessent > 0)
}
}
it should "be able to ban and clear the ban of a subnet" in {
val loopBack = URI.create("http://127.0.0.1")
for {
(client1, _) <- BitcoindRpcTestUtil.createNodePair(
clientAccum = clientAccum)
_ <- client1.setBan(loopBack, SetBanCommand.Add)
list <- client1.listBanned
_ <- client1.setBan(loopBack, SetBanCommand.Remove)
newList <- client1.listBanned
} yield {
assert(list.length == 1)
assert(list.head.address.getAuthority == loopBack.getAuthority)
assert(list.head.banned_until - list.head.ban_created == UInt32(86400))
assert(newList.isEmpty)
}
}
it should "be able to get the difficulty on the network" in {
for {
client <- clientF
difficulty <- client.getDifficulty
} yield {
assert(difficulty > 0)
assert(difficulty < 1)
}
}
it should "be able to deactivate and activate the network" in {
for {
client <- clientF
_ <- client.setNetworkActive(false)
firstInfo <- client.getNetworkInfo
_ <- client.setNetworkActive(true)
secondInfo <- client.getNetworkInfo
} yield {
assert(!firstInfo.networkactive)
assert(secondInfo.networkactive)
}
}
it should "be able to clear banned subnets" in {
for {
(client1, _) <- BitcoindRpcTestUtil.createNodePair(
clientAccum = clientAccum)
_ <- client1.setBan(URI.create("http://127.0.0.1"), SetBanCommand.Add)
_ <- client1.setBan(URI.create("http://127.0.0.2"), SetBanCommand.Add)
list <- client1.listBanned
_ <- client1.clearBanned()
newList <- client1.listBanned
} yield {
assert(list.length == 2)
assert(newList.isEmpty)
}
}
it should "be able to add and remove a node" in {
for {
(freshClient, otherFreshClient) <- BitcoindRpcTestUtil
.createUnconnectedNodePair(clientAccum = clientAccum)
uri = otherFreshClient.getDaemon.uri
_ <- freshClient.addNode(uri, AddNodeArgument.Add)
_ <- BitcoindRpcTestUtil.awaitConnection(freshClient, otherFreshClient)
info <- freshClient.getAddedNodeInfo(otherFreshClient.getDaemon.uri)
_ <- freshClient.addNode(uri, AddNodeArgument.Remove)
newInfo <- otherFreshClient.getAddedNodeInfo
} yield {
assert(info.length == 1)
assert(info.head.addednode == otherFreshClient.getDaemon.uri)
assert(info.head.connected.contains(true))
assert(newInfo.isEmpty)
}
}
it should "be able to add and disconnect a node" in {
for {
(freshClient, otherFreshClient) <- BitcoindRpcTestUtil
.createUnconnectedNodePair(clientAccum = clientAccum)
uri = otherFreshClient.getDaemon.uri
_ <- freshClient.addNode(uri, AddNodeArgument.Add)
_ <- BitcoindRpcTestUtil.awaitConnection(freshClient, otherFreshClient)
info <- freshClient.getAddedNodeInfo(otherFreshClient.getDaemon.uri)
_ <- freshClient.disconnectNode(otherFreshClient.getDaemon.uri)
_ <- BitcoindRpcTestUtil.awaitDisconnected(freshClient, otherFreshClient)
newInfo <- freshClient.getAddedNodeInfo(otherFreshClient.getDaemon.uri)
} yield {
assert(info.head.connected.contains(true))
assert(newInfo.head.connected.contains(false))
}
}
it should "be able to get the connection count" in {
for {
(freshClient, otherFreshClient) <- BitcoindRpcTestUtil
.createUnconnectedNodePair()
connectionPre <- freshClient.getConnectionCount
_ <- freshClient.addNode(otherFreshClient.getDaemon.uri,
AddNodeArgument.Add)
_ <- BitcoindRpcTestUtil.awaitConnection(freshClient, otherFreshClient)
connectionPost <- otherFreshClient.getConnectionCount
} yield {
assert(connectionPre == 0)
assert(connectionPost == 1)
}
}
it should "be able to submit a new block" in {
for {
(client1, client2) <- BitcoindRpcTestUtil.createUnconnectedNodePair(
clientAccum = clientAccum)
hash <- client2.generate(1)
block <- client2.getBlockRaw(hash.head)
preCount1 <- client1.getBlockCount
preCount2 <- client2.getBlockCount
_ <- client1.submitBlock(block)
postCount1 <- client1.getBlockCount
postCount2 <- client2.getBlockCount
hash1 <- client1.getBlockHash(postCount1)
hash2 <- client2.getBlockHash(postCount2)
} yield {
assert(preCount1 != preCount2)
assert(postCount1 == postCount2)
assert(hash1 == hash2)
}
}
}

View File

@ -0,0 +1,267 @@
package org.bitcoins.rpc.common
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script.{
P2SHScriptSignature,
ScriptPubKey,
ScriptSignature
}
import org.bitcoins.core.protocol.transaction.{
TransactionInput,
TransactionOutPoint
}
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, RpcOpts}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class RawTransactionRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodePair(clientAccum = clientAccum)
behavior of "RawTransactionRpc"
it should "be able to fund a raw transaction" in {
for {
(client, otherClient) <- clientsF
address <- otherClient.getNewAddress
transactionWithoutFunds <- client
.createRawTransaction(Vector.empty, Map(address -> Bitcoins(1)))
transactionResult <- client.fundRawTransaction(transactionWithoutFunds)
transaction = transactionResult.hex
inputTransaction <- client
.getRawTransaction(transaction.inputs.head.previousOutput.txId.flip)
} yield {
assert(transaction.inputs.length == 1)
val inputTxSats =
inputTransaction
.vout(transaction.inputs.head.previousOutput.vout.toInt)
.value
val txResultSats =
transactionResult.fee +
transaction.outputs.head.value +
transaction.outputs(1).value
assert(txResultSats == inputTxSats)
}
}
it should "be able to decode a raw transaction" in {
for {
(client, otherClient) <- clientsF
transaction <- BitcoindRpcTestUtil
.createRawCoinbaseTransaction(client, otherClient)
rpcTransaction <- client.decodeRawTransaction(transaction)
} yield {
assert(rpcTransaction.txid == transaction.txIdBE)
assert(rpcTransaction.locktime == transaction.lockTime)
assert(rpcTransaction.size == transaction.size)
assert(rpcTransaction.version == transaction.version.toInt)
assert(rpcTransaction.vsize == transaction.vsize)
}
}
it should "be able to get a raw transaction using both rpcs available" in {
for {
(client, _) <- clientsF
block <- BitcoindRpcTestUtil.getFirstBlock(client)
txid = block.tx.head.txid
transaction1 <- client.getRawTransaction(txid)
transaction2 <- client.getTransaction(txid)
} yield {
assert(transaction1.txid == transaction2.txid)
assert(transaction1.confirmations.contains(transaction2.confirmations))
assert(transaction1.hex == transaction2.hex)
assert(transaction1.blockhash.isDefined)
assert(transaction2.blockhash.isDefined)
assert(transaction1.blockhash == transaction2.blockhash)
}
}
it should "be able to create a raw transaction" in {
for {
(client, otherClient) <- clientsF
blocks <- client.generate(2)
firstBlock <- client.getBlock(blocks(0))
transaction0 <- client.getTransaction(firstBlock.tx(0))
secondBlock <- client.getBlock(blocks(1))
transaction1 <- client.getTransaction(secondBlock.tx(0))
address <- otherClient.getNewAddress
input0 = TransactionOutPoint(transaction0.txid.flip,
UInt32(transaction0.blockindex.get))
input1 = TransactionOutPoint(transaction1.txid.flip,
UInt32(transaction1.blockindex.get))
transaction <- {
val sig: ScriptSignature = ScriptSignature.empty
val inputs = Vector(TransactionInput(input0, sig, UInt32(1)),
TransactionInput(input1, sig, UInt32(2)))
val outputs = Map(address -> Bitcoins(1))
client.createRawTransaction(inputs, outputs)
}
} yield {
val inputs = transaction.inputs
assert(inputs.head.sequence == UInt32(1))
assert(inputs(1).sequence == UInt32(2))
assert(inputs.head.previousOutput.txId == input0.txId)
assert(inputs(1).previousOutput.txId == input1.txId)
}
}
it should "be able to send a raw transaction to the mem pool" in {
for {
(client, otherClient) <- clientsF
rawTx <- BitcoindRpcTestUtil.createRawCoinbaseTransaction(client,
otherClient)
signedTransaction <- BitcoindRpcTestUtil.signRawTransaction(client, rawTx)
_ <- client.generate(100) // Can't spend coinbase until depth 100
_ <- client.sendRawTransaction(signedTransaction.hex,
allowHighFees = true)
} yield succeed
}
it should "be able to sign a raw transaction" in {
for {
(client, _) <- clientsF
address <- client.getNewAddress
pubkey <- BitcoindRpcTestUtil.getPubkey(client, address)
multisig <- client
.addMultiSigAddress(1, Vector(Left(pubkey.get)))
txid <- BitcoindRpcTestUtil
.fundBlockChainTransaction(client, multisig.address, Bitcoins(1.2))
rawTx <- client.getTransaction(txid)
tx <- client.decodeRawTransaction(rawTx.hex)
output = tx.vout
.find(output => output.value == Bitcoins(1.2))
.get
newAddress <- client.getNewAddress
rawCreatedTx <- {
val input =
TransactionInput(TransactionOutPoint(txid.flip, UInt32(output.n)),
P2SHScriptSignature(multisig.redeemScript.hex),
UInt32.max - UInt32.one)
client
.createRawTransaction(Vector(input), Map(newAddress -> Bitcoins(1.1)))
}
result <- {
val utxoDeps = Vector(
RpcOpts.SignRawTransactionOutputParameter(
txid,
output.n,
ScriptPubKey.fromAsmHex(output.scriptPubKey.hex),
Some(multisig.redeemScript),
amount = Some(Bitcoins(1.2))))
BitcoindRpcTestUtil.signRawTransaction(
client,
rawCreatedTx,
utxoDeps
)
}
} yield assert(result.complete)
}
it should "be able to combine raw transactions" in {
for {
(client, otherClient) <- clientsF
address1 <- client.getNewAddress
address2 <- otherClient.getNewAddress
pub1 <- BitcoindRpcTestUtil.getPubkey(client, address1)
pub2 <- BitcoindRpcTestUtil.getPubkey(otherClient, address2)
keys = Vector(Left(pub1.get), Left(pub2.get))
multisig <- client.addMultiSigAddress(2, keys)
_ <- otherClient.addMultiSigAddress(2, keys)
txid <- BitcoindRpcTestUtil.fundBlockChainTransaction(client,
multisig.address,
Bitcoins(1.2))
rawTx <- client.getTransaction(txid)
tx <- client.decodeRawTransaction(rawTx.hex)
output = tx.vout
.find(output => output.value == Bitcoins(1.2))
.get
address3 <- client.getNewAddress
ctx <- {
val input =
TransactionInput(TransactionOutPoint(txid.flip, UInt32(output.n)),
P2SHScriptSignature(multisig.redeemScript.hex),
UInt32.max - UInt32.one)
otherClient
.createRawTransaction(Vector(input), Map(address3 -> Bitcoins(1.1)))
}
txOpts = {
val scriptPubKey =
ScriptPubKey.fromAsmHex(output.scriptPubKey.hex)
val utxoDep =
RpcOpts.SignRawTransactionOutputParameter(
txid,
output.n,
scriptPubKey,
Some(multisig.redeemScript),
amount = Some(Bitcoins(1.2)))
Vector(utxoDep)
}
partialTx1 <- BitcoindRpcTestUtil.signRawTransaction(client, ctx, txOpts)
partialTx2 <- BitcoindRpcTestUtil.signRawTransaction(otherClient,
ctx,
txOpts)
combinedTx <- {
val txs = Vector(partialTx1.hex, partialTx2.hex)
client.combineRawTransaction(txs)
}
_ <- client.sendRawTransaction(combinedTx)
} yield {
assert(!partialTx1.complete)
assert(partialTx1.hex != ctx)
assert(!partialTx2.complete)
assert(partialTx2.hex != ctx)
}
}
it should "fail to abandon a transaction which has not been sent" in {
clientsF.flatMap {
case (client, otherClient) =>
otherClient.getNewAddress.flatMap { address =>
client
.createRawTransaction(Vector(), Map(address -> Bitcoins(1)))
.flatMap { tx =>
recoverToSucceededIf[RuntimeException](
client.abandonTransaction(tx.txId))
}
}
}
}
it should "be able to get a raw transaction in serialized form from the mem pool" in {
for {
(client, otherClient) <- clientsF
sentTx <- BitcoindRpcTestUtil.sendCoinbaseTransaction(client, otherClient)
rawTx <- client.getRawTransactionRaw(sentTx.txid)
} yield assert(rawTx.txIdBE == sentTx.txid)
}
}

View File

@ -0,0 +1,57 @@
package org.bitcoins.rpc.common
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, RpcOpts}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class UTXORpcTest extends BitcoindRpcTest {
lazy val clientF: Future[BitcoindRpcClient] =
BitcoindRpcTestUtil.startedBitcoindRpcClient(clientAccum = clientAccum)
behavior of "UTXORpc"
it should "be able to list utxos" in {
for {
client <- clientF
unspent <- client.listUnspent
} yield assert(unspent.nonEmpty)
}
it should "be able to lock and unlock utxos as well as list locked utxos" in {
for {
client <- clientF
unspent <- client.listUnspent
txid1 = unspent(0).txid
txid2 = unspent(1).txid
param = {
val vout1 = unspent(0).vout
val vout2 = unspent(1).vout
Vector(RpcOpts.LockUnspentOutputParameter(txid1, vout1),
RpcOpts.LockUnspentOutputParameter(txid2, vout2))
}
firstSuccess <- client.lockUnspent(unlock = false, param)
locked <- client.listLockUnspent
secondSuccess <- client.lockUnspent(unlock = true, param)
newLocked <- client.listLockUnspent
} yield {
assert(firstSuccess)
assert(locked.length == 2)
assert(locked(0).txId.flip == txid1)
assert(locked(1).txId.flip == txid2)
assert(secondSuccess)
assert(newLocked.isEmpty)
}
}
it should "be able to get utxo info" in {
for {
client <- clientF
block <- BitcoindRpcTestUtil.getFirstBlock(client)
info1 <- client.getTxOut(block.tx.head.txid, 0)
} yield assert(info1.coinbase)
}
}

View File

@ -0,0 +1,44 @@
package org.bitcoins.rpc.common
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.rpc.jsonmodels.RpcScriptType
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class UtilRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[(BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodePair(clientAccum = clientAccum)
behavior of "RpcUtilTest"
it should "be able to validate a bitcoin address" in {
for {
(client, otherClient) <- clientsF
address <- otherClient.getNewAddress
validation <- client.validateAddress(address)
} yield assert(validation.isvalid)
}
it should "be able to decode a reedem script" in {
val ecPrivKey1 = ECPrivateKey.freshPrivateKey
val pubKey1 = ecPrivKey1.publicKey
for {
(client, _) <- clientsF
address <- client.getNewAddress(addressType = AddressType.Legacy)
multisig <- client
.addMultiSigAddress(
2,
Vector(Left(pubKey1), Right(address.asInstanceOf[P2PKHAddress])))
decoded <- client.decodeScript(multisig.redeemScript)
} yield {
assert(decoded.reqSigs.contains(2))
assert(decoded.typeOfScript.contains(RpcScriptType.MULTISIG))
assert(decoded.addresses.get.contains(address))
}
}
}

View File

@ -0,0 +1,479 @@
package org.bitcoins.rpc.common
import java.io.File
import java.util.Scanner
import org.bitcoins.core.crypto.{ECPrivateKey, ECPublicKey}
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.number.{Int64, UInt32}
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.core.protocol.script.ScriptSignature
import org.bitcoins.core.protocol.transaction.{
TransactionInput,
TransactionOutPoint
}
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.rpc.client.common.{
BitcoindRpcClient,
BitcoindVersion,
RpcOpts
}
import org.bitcoins.rpc.jsonmodels.RpcAddress
import org.bitcoins.rpc.util.RpcUtil
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.async.Async.{async, await}
import scala.concurrent.Future
class WalletRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[
(BitcoindRpcClient, BitcoindRpcClient, BitcoindRpcClient)] =
BitcoindRpcTestUtil.createNodeTriple(clientAccum = clientAccum)
// This client's wallet is encrypted
lazy val walletClientF: Future[BitcoindRpcClient] = clientsF.flatMap { _ =>
val walletClient = new BitcoindRpcClient(BitcoindRpcTestUtil.instance())
clientAccum += walletClient
for {
_ <- walletClient.start()
_ <- walletClient.generate(200)
_ <- walletClient.encryptWallet(password)
_ <- walletClient.stop()
_ <- RpcUtil.awaitServerShutdown(walletClient)
_ <- Future {
// Very rarely we are prevented from starting the client again because Core
// hasn't released its locks on the datadir. This is prevent that.
Thread.sleep(1000)
}
_ <- walletClient.start()
} yield walletClient
}
var password = "password"
behavior of "WalletRpc"
it should "be able to dump the wallet" in {
for {
(client, _, _) <- clientsF
result <- {
val datadir = client.getDaemon.authCredentials.datadir
client.dumpWallet(datadir + "/test.dat")
}
} yield {
assert(result.filename.exists)
assert(result.filename.isFile)
}
}
it should "be able to list wallets" in {
for {
(client, _, _) <- clientsF
wallets <- client.listWallets
} yield {
val expectedFileName =
if (client.instance.getVersion == BitcoindVersion.V17) ""
else "wallet.dat"
assert(wallets == Vector(expectedFileName))
}
}
it should "be able to backup the wallet" in {
for {
(client, _, _) <- clientsF
_ <- {
val datadir = client.getDaemon.authCredentials.datadir
client.backupWallet(datadir + "/backup.dat")
}
} yield {
val datadir = client.getDaemon.authCredentials.datadir
val file = new File(datadir + "/backup.dat")
assert(file.exists)
assert(file.isFile)
}
}
it should "be able to lock and unlock the wallet" in {
for {
walletClient <- walletClientF
_ <- walletClient.walletLock()
_ <- walletClient.walletPassphrase(password, 1000)
info <- walletClient.getWalletInfo
_ = assert(info.unlocked_until.nonEmpty)
_ = assert(info.unlocked_until.get > 0)
_ <- walletClient.walletLock()
newInfo <- walletClient.getWalletInfo
} yield assert(newInfo.unlocked_until.contains(0))
}
it should "be able to get an address from bitcoind" in {
for {
(client, _, _) <- clientsF
_ <- {
val addrFuts =
List(client.getNewAddress,
client.getNewAddress(AddressType.Bech32),
client.getNewAddress(AddressType.P2SHSegwit),
client.getNewAddress(AddressType.Legacy))
Future.sequence(addrFuts)
}
} yield succeed
}
it should "be able to get a new raw change address" in {
for {
(client, _, _) <- clientsF
_ <- {
val addrFuts =
List(
client.getRawChangeAddress,
client.getRawChangeAddress(AddressType.Legacy),
client.getRawChangeAddress(AddressType.Bech32),
client.getRawChangeAddress(AddressType.P2SHSegwit)
)
Future.sequence(addrFuts)
}
} yield succeed
}
it should "be able to get the amount recieved by some address" in {
for {
(client, _, _) <- clientsF
address <- client.getNewAddress
amount <- client.getReceivedByAddress(address)
} yield assert(amount == Bitcoins(0))
}
it should "be able to get the unconfirmed balance" in {
for {
(client, _, _) <- clientsF
balance <- client.getUnconfirmedBalance
transaction <- BitcoindRpcTestUtil.sendCoinbaseTransaction(client, client)
newBalance <- client.getUnconfirmedBalance
} yield {
assert(balance == Bitcoins(0))
assert(newBalance == transaction.amount)
}
}
it should "be able to get the wallet info" in {
for {
(client, _, _) <- clientsF
info <- client.getWalletInfo
} yield {
assert(info.balance.toBigDecimal > 0)
assert(info.txcount > 0)
assert(info.keypoolsize > 0)
assert(!info.unlocked_until.contains(0))
}
}
it should "be able to refill the keypool" in {
for {
(client, _, _) <- clientsF
info <- client.getWalletInfo
_ <- client.keyPoolRefill(info.keypoolsize + 1)
newInfo <- client.getWalletInfo
} yield assert(newInfo.keypoolsize == info.keypoolsize + 1)
}
it should "be able to change the wallet password" in {
val newPass = "new_password"
for {
walletClient <- walletClientF
_ <- walletClient.walletLock()
_ <- walletClient.walletPassphraseChange(password, newPass)
_ = {
password = newPass
}
_ <- walletClient.walletPassphrase(password, 1000)
info <- walletClient.getWalletInfo
_ <- walletClient.walletLock()
newInfo <- walletClient.getWalletInfo
} yield {
assert(info.unlocked_until.nonEmpty)
assert(info.unlocked_until.get > 0)
assert(newInfo.unlocked_until.contains(0))
}
}
it should "be able to import funds without rescan and then remove them" in async {
val (client, otherClient, thirdClient) = await(clientsF)
val address = await(thirdClient.getNewAddress)
val privKey = await(thirdClient.dumpPrivKey(address))
val txidF =
BitcoindRpcTestUtil
.fundBlockChainTransaction(client, address, Bitcoins(1.5))
val txid = await(txidF)
await(client.generate(1))
val tx = await(client.getTransaction(txid))
val proof = await(client.getTxOutProof(Vector(txid)))
val balanceBefore = await(otherClient.getBalance)
await(otherClient.importPrivKey(privKey, rescan = false))
await(otherClient.importPrunedFunds(tx.hex, proof))
val balanceAfter = await(otherClient.getBalance)
assert(balanceAfter == balanceBefore + Bitcoins(1.5))
val addressInfo = await(otherClient.validateAddress(address))
if (otherClient.instance.getVersion == BitcoindVersion.V16) {
assert(addressInfo.ismine.contains(true))
}
await(otherClient.removePrunedFunds(txid))
val balance = await(otherClient.getBalance)
assert(balance == balanceBefore)
}
it should "be able to list address groupings" in {
for {
(client, _, _) <- clientsF
address <- client.getNewAddress
_ <- BitcoindRpcTestUtil
.fundBlockChainTransaction(client, address, Bitcoins(1.25))
groupings <- client.listAddressGroupings
block <- BitcoindRpcTestUtil.getFirstBlock(client)
} yield {
val rpcAddress =
groupings.find(vec => vec.head.address == address).get.head
assert(rpcAddress.address == address)
assert(rpcAddress.balance == Bitcoins(1.25))
val firstAddress =
block.tx.head.vout.head.scriptPubKey.addresses.get.head
val maxGroup =
groupings
.max(Ordering.by[Vector[RpcAddress], BigDecimal](addr =>
addr.head.balance.toBigDecimal))
.head
assert(maxGroup.address == firstAddress)
}
}
it should "be able to send to an address" in {
for {
(client, otherClient, _) <- clientsF
address <- otherClient.getNewAddress
txid <- client.sendToAddress(address, Bitcoins(1))
transaction <- client.getTransaction(txid)
} yield {
assert(transaction.amount == Bitcoins(-1))
assert(transaction.details.head.address.contains(address))
}
}
it should "be able to send btc to many addresses" in {
for {
(client, otherClient, _) <- clientsF
address1 <- otherClient.getNewAddress
address2 <- otherClient.getNewAddress
txid <- client
.sendMany(Map(address1 -> Bitcoins(1), address2 -> Bitcoins(2)))
transaction <- client.getTransaction(txid)
} yield {
assert(transaction.amount == Bitcoins(-3))
assert(transaction.details.exists(_.address.contains(address1)))
assert(transaction.details.exists(_.address.contains(address2)))
}
}
it should "be able to list transactions by receiving addresses" in {
for {
(client, otherClient, _) <- clientsF
address <- otherClient.getNewAddress
txid <- BitcoindRpcTestUtil
.fundBlockChainTransaction(client, address, Bitcoins(1.5))
receivedList <- otherClient.listReceivedByAddress()
} yield {
val entryList =
receivedList.filter(entry => entry.address == address)
assert(entryList.length == 1)
val entry = entryList.head
assert(entry.txids.head == txid)
assert(entry.address == address)
assert(entry.amount == Bitcoins(1.5))
assert(entry.confirmations == 1)
}
}
it should "be able to import an address" in {
for {
(client, otherClient, _) <- clientsF
address <- client.getNewAddress
_ <- otherClient.importAddress(address)
txid <- BitcoindRpcTestUtil.fundBlockChainTransaction(client,
address,
Bitcoins(1.5))
list <- otherClient.listReceivedByAddress(includeWatchOnly = true)
} yield {
val entry =
list
.find(addr => addr.involvesWatchonly.contains(true))
.get
assert(entry.address == address)
assert(entry.involvesWatchonly.contains(true))
assert(entry.amount == Bitcoins(1.5))
assert(entry.txids.head == txid)
}
}
it should "be able to get the balance" in {
for {
(client, _, _) <- clientsF
balance <- client.getBalance
_ <- client.generate(1)
newBalance <- client.getBalance
} yield {
assert(balance.toBigDecimal > 0)
assert(balance.toBigDecimal < newBalance.toBigDecimal)
}
}
it should "be able to dump a private key" in {
for {
(client, _, _) <- clientsF
address <- client.getNewAddress
_ <- client.dumpPrivKey(address)
} yield succeed
}
it should "be able to import a private key" in {
val ecPrivateKey = ECPrivateKey.freshPrivateKey
val publicKey = ecPrivateKey.publicKey
val address = P2PKHAddress(publicKey, networkParam)
for {
(client, _, _) <- clientsF
_ <- client.importPrivKey(ecPrivateKey, rescan = false)
key <- client.dumpPrivKey(address)
result <- client
.dumpWallet(
client.getDaemon.authCredentials.datadir + "/wallet_dump.dat")
} yield {
assert(key == ecPrivateKey)
val reader = new Scanner(result.filename)
var found = false
while (reader.hasNext) {
if (reader.next == ecPrivateKey.toWIF(networkParam)) {
found = true
}
}
assert(found)
}
}
it should "be able to import a public key" in {
val pubKey = ECPublicKey.freshPublicKey
for {
(client, _, _) <- clientsF
_ <- client.importPubKey(pubKey)
} yield succeed
}
it should "be able to import multiple addresses with importMulti" in {
val privKey = ECPrivateKey.freshPrivateKey
val address1 = P2PKHAddress(privKey.publicKey, networkParam)
val privKey1 = ECPrivateKey.freshPrivateKey
val privKey2 = ECPrivateKey.freshPrivateKey
for {
(client, _, _) <- clientsF
firstResult <- client
.createMultiSig(2, Vector(privKey1.publicKey, privKey2.publicKey))
address2 = firstResult.address
secondResult <- client
.importMulti(
Vector(
RpcOpts.ImportMultiRequest(RpcOpts.ImportMultiAddress(address1),
UInt32(0)),
RpcOpts.ImportMultiRequest(RpcOpts.ImportMultiAddress(address2),
UInt32(0))),
rescan = false
)
} yield {
assert(secondResult.length == 2)
assert(secondResult(0).success)
assert(secondResult(1).success)
}
}
it should "be able to import a wallet" in {
for {
(client, _, _) <- clientsF
walletClient <- walletClientF
address <- client.getNewAddress
walletFile = client.getDaemon.authCredentials.datadir + "/client_wallet.dat"
fileResult <- client.dumpWallet(walletFile)
_ <- walletClient.walletPassphrase(password, 1000)
_ <- walletClient.importWallet(walletFile)
_ <- walletClient.dumpPrivKey(address)
} yield assert(fileResult.filename.exists)
}
it should "be able to set the tx fee" in {
for {
(client, _, _) <- clientsF
success <- client.setTxFee(Bitcoins(0.01))
info <- client.getWalletInfo
} yield {
assert(success)
assert(info.paytxfee == SatoshisPerByte(Satoshis(Int64(1000))))
}
}
it should "be able to bump a mem pool tx fee" in {
for {
(client, otherClient, _) <- clientsF
address <- otherClient.getNewAddress
unspent <- client.listUnspent
changeAddress <- client.getRawChangeAddress
rawTx <- {
val output =
unspent.find(output => output.amount.toBigDecimal > 1).get
val input =
TransactionInput(
TransactionOutPoint(output.txid.flip, UInt32(output.vout)),
ScriptSignature.empty,
UInt32.max - UInt32(2))
val inputs = Vector(input)
val outputs =
Map(address -> Bitcoins(0.5),
changeAddress -> Bitcoins(output.amount.toBigDecimal - 0.55))
client.createRawTransaction(inputs, outputs)
}
stx <- BitcoindRpcTestUtil.signRawTransaction(client, rawTx)
txid <- client.sendRawTransaction(stx.hex, allowHighFees = true)
tx <- client.getTransaction(txid)
bumpedTx <- client.bumpFee(txid)
} yield assert(tx.fee.get < bumpedTx.fee)
}
}

View File

@ -0,0 +1,210 @@
package org.bitcoins.rpc.v16
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, ECPrivateKey}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptSignature}
import org.bitcoins.core.protocol.transaction.{
TransactionConstants,
TransactionInput,
TransactionOutPoint
}
import org.bitcoins.rpc.client.common.RpcOpts.SignRawTransactionOutputParameter
import org.bitcoins.rpc.client.v16.BitcoindV16RpcClient
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.async.Async.{async, await}
import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
import scala.util.Properties
class BitcoindV16RpcClientTest extends BitcoindRpcTest {
lazy val clientsF: Future[(BitcoindV16RpcClient, BitcoindV16RpcClient)] =
BitcoindRpcTestUtil.createNodePairV16(clientAccum)
behavior of "BitoindV16RpcClient"
it should "be able to sign a raw transaction" in {
for {
(client, otherClient) <- clientsF
addr <- client.getNewAddress
_ <- otherClient.sendToAddress(addr, Bitcoins.one)
_ <- otherClient.generate(6)
peers <- client.getPeerInfo
_ = assert(peers.exists(_.networkInfo.addr == otherClient.getDaemon.uri))
recentBlock <- otherClient.getBestBlockHash
_ <- AsyncUtil.retryUntilSatisfiedF(
() => BitcoindRpcTestUtil.hasSeenBlock(client, recentBlock),
1.second)
(utxoTxid, utxoVout) <- client.listUnspent
.map(_.filter(_.address.contains(addr)))
.map(_.head)
.map(utxo => (utxo.txid, utxo.vout))
newAddress <- client.getNewAddress
rawTx <- {
val outPoint = TransactionOutPoint(utxoTxid.flip, UInt32(utxoVout))
val input = TransactionInput(outPoint,
ScriptSignature.empty,
TransactionConstants.sequence)
val outputs = Map(newAddress -> Bitcoins(0.5))
client.createRawTransaction(Vector(input), outputs)
}
signedRawTx <- client.signRawTransaction(rawTx)
} yield {
assert(signedRawTx.complete)
}
}
// copied form the equivalent test in BitcoindV17RpcClientTest
it should "be able to sign a raw transaction with private keys" in {
val privkeys: List[ECPrivateKey] =
List("cUeKHd5orzT3mz8P9pxyREHfsWtVfgsfDjiZZBcjUBAaGk1BTj7N",
"cVKpPfVKSJxKqVpE9awvXNWuLHCa5j5tiE7K6zbUSptFpTEtiFrA")
.map(ECPrivateKey.fromWIFToPrivateKey)
val txids =
List("9b907ef1e3c26fc71fe4a4b3580bc75264112f95050014157059c736f0202e71",
"83a4f6a6b73660e13ee6cb3c6063fa3759c50c9b7521d0536022961898f4fb02")
.map(DoubleSha256DigestBE.fromHex)
val vouts = List(0, 0)
val inputs: Vector[TransactionInput] = txids
.zip(vouts)
.map {
case (txid, vout) =>
TransactionInput.fromTxidAndVout(txid, UInt32(vout))
}
.toVector
val address =
BitcoinAddress.fromStringExn("mpLQjfK79b7CCV4VMJWEWAj5Mpx8Up5zxB")
val outputs: Map[BitcoinAddress, Bitcoins] =
Map(address -> Bitcoins(0.1))
val scriptPubKeys =
List("76a91460baa0f494b38ce3c940dea67f3804dc52d1fb9488ac",
"76a914669b857c03a5ed269d5d85a1ffac9ed5d663072788ac")
.map(ScriptPubKey.fromAsmHex)
val utxoDeps = inputs.zip(scriptPubKeys).map {
case (input, pubKey) =>
SignRawTransactionOutputParameter.fromTransactionInput(input, pubKey)
}
for {
(client, _) <- clientsF
rawTx <- client.createRawTransaction(inputs, outputs)
signed <- client.signRawTransaction(rawTx, utxoDeps, privkeys.toVector)
} yield assert(signed.complete)
}
it should "be able to send from an account to an addresss" in {
for {
(client, otherClient) <- clientsF
address <- otherClient.getNewAddress
txid <- client.sendFrom("", address, Bitcoins(1))
transaction <- client.getTransaction(txid)
} yield {
assert(transaction.amount == Bitcoins(-1))
assert(transaction.details.head.address.contains(address))
}
}
it should "be able to get the amount received by an account and list amounts received by all accounts" in async {
val (client, otherClient) = await(clientsF)
val ourAccount = "another_new_account"
val emptyAccount = "empty_account"
val ourAccountAddress = await(client.getNewAddress(ourAccount))
await(BitcoindRpcTestUtil
.fundBlockChainTransaction(otherClient, ourAccountAddress, Bitcoins(1.5)))
val accountlessAddress = await(client.getNewAddress)
val sendAmt = Bitcoins(1.5)
val _ = await(
BitcoindRpcTestUtil
.fundBlockChainTransaction(otherClient, accountlessAddress, sendAmt))
if (Properties.isMac) Thread.sleep(10000)
val ourAccountAmount = await(client.getReceivedByAccount(ourAccount))
assert(ourAccountAmount == sendAmt)
val receivedByAccount = await(client.listReceivedByAccount())
val ourAccountOpt =
receivedByAccount
.find(_.account == ourAccount)
assert(ourAccountOpt.isDefined)
assert(ourAccountOpt.get.amount == Bitcoins(1.5))
val accountLessOpt =
receivedByAccount
.find(_.account == "")
assert(accountLessOpt.isDefined)
assert(accountLessOpt.get.amount > Bitcoins(0))
assert(!receivedByAccount.exists(_.account == emptyAccount))
val accounts = await(client.listAccounts())
assert(accounts(ourAccount) == Bitcoins(1.5))
assert(accounts("") > Bitcoins(0))
assert(!accounts.keySet.contains(emptyAccount))
}
it should "be able to get and set the account for a given address" in {
val account1 = "account_1"
val account2 = "account_2"
for {
(client, _) <- clientsF
address <- client.getNewAddress(account1)
acc1 <- client.getAccount(address)
_ <- client.setAccount(address, account2)
acc2 <- client.getAccount(address)
} yield {
assert(acc1 == account1)
assert(acc2 == account2)
}
}
it should "be able to get all addresses belonging to an account" in {
for {
(client, _) <- clientsF
address <- client.getNewAddress
addresses <- client.getAddressesByAccount("")
} yield assert(addresses.contains(address))
}
it should "be able to get an account's address" in {
val account = "a_new_account"
for {
(client, _) <- clientsF
address <- client.getAccountAddress(account)
result <- client.getAccount(address)
} yield assert(result == account)
}
it should "be able to move funds from one account to another" in {
val account = "move_account"
for {
(client, _) <- clientsF
success <- client.move("", account, Bitcoins(1))
map <- client.listAccounts()
} yield {
assert(success)
assert(map(account) == Bitcoins(1))
}
}
}

View File

@ -0,0 +1,230 @@
package org.bitcoins.rpc.v17
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, ECPrivateKey}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionInput
import org.bitcoins.rpc.client.common.RpcOpts.{
AddressType,
LabelPurpose,
SignRawTransactionOutputParameter
}
import org.bitcoins.rpc.client.v17.BitcoindV17RpcClient
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import org.joda.time.DateTime
import scala.concurrent.Future
class BitcoindV17RpcClientTest extends BitcoindRpcTest {
val usedLabel = "used_label"
val unusedLabel = "unused_label"
val clientsF: Future[(BitcoindV17RpcClient, BitcoindV17RpcClient)] =
BitcoindRpcTestUtil.createNodePairV17(clientAccum)
behavior of "BitcoindV17RpcClient"
it should "test mempool acceptance" in {
for {
(client, otherClient) <- clientsF
tx <- BitcoindRpcTestUtil.createRawCoinbaseTransaction(client,
otherClient)
acceptance <- client.testMempoolAccept(tx)
} yield {
assert(acceptance.rejectReason.isEmpty == acceptance.allowed)
}
}
it should "sign a raw transaction with wallet keys" in {
for {
(client, otherClient) <- clientsF
rawTx <- BitcoindRpcTestUtil.createRawCoinbaseTransaction(client,
otherClient)
signedTx <- client.signRawTransactionWithWallet(rawTx)
} yield assert(signedTx.complete)
}
// copied from Bitcoin Core: https://github.com/bitcoin/bitcoin/blob/fa6180188b8ab89af97860e6497716405a48bab6/test/functional/rpc_signrawtransaction.py
it should "sign a raw transaction with private keys" in {
val privkeys =
List("cUeKHd5orzT3mz8P9pxyREHfsWtVfgsfDjiZZBcjUBAaGk1BTj7N",
"cVKpPfVKSJxKqVpE9awvXNWuLHCa5j5tiE7K6zbUSptFpTEtiFrA")
.map(ECPrivateKey.fromWIFToPrivateKey)
val txids =
List("9b907ef1e3c26fc71fe4a4b3580bc75264112f95050014157059c736f0202e71",
"83a4f6a6b73660e13ee6cb3c6063fa3759c50c9b7521d0536022961898f4fb02")
.map(DoubleSha256DigestBE.fromHex)
val vouts = List(0, 0)
val inputs: Vector[TransactionInput] = txids
.zip(vouts)
.map {
case (txid, vout) =>
TransactionInput.fromTxidAndVout(txid, UInt32(vout))
}
.toVector
val address =
BitcoinAddress.fromStringExn("mpLQjfK79b7CCV4VMJWEWAj5Mpx8Up5zxB")
val outputs: Map[BitcoinAddress, Bitcoins] =
Map(address -> Bitcoins(0.1))
val scriptPubKeys =
List("76a91460baa0f494b38ce3c940dea67f3804dc52d1fb9488ac",
"76a914669b857c03a5ed269d5d85a1ffac9ed5d663072788ac")
.map(ScriptPubKey.fromAsmHex)
val utxoDeps = inputs.zip(scriptPubKeys).map {
case (input, pubKey) =>
SignRawTransactionOutputParameter.fromTransactionInput(input, pubKey)
}
for {
(client, _) <- clientsF
rawTx <- client.createRawTransaction(inputs, outputs)
signed <- client.signRawTransactionWithKey(rawTx,
privkeys.toVector,
utxoDeps)
} yield assert(signed.complete)
}
it should "be able to get the address info for a given address" in {
for {
(client, _) <- clientsF
addr <- client.getNewAddress
info <- client.getAddressInfo(addr)
} yield assert(info.timestamp.exists(_.dayOfYear == DateTime.now.dayOfYear))
}
it should "be able to get the address info for a given P2SHSegwit address" in {
for {
(client, _) <- clientsF
addr <- client.getNewAddress(addressType = AddressType.P2SHSegwit)
info <- client.getAddressInfo(addr)
} yield assert(info.timestamp.exists(_.dayOfYear == DateTime.now.dayOfYear))
}
it should "be able to get the address info for a given Legacy address" in {
for {
(client, _) <- clientsF
addr <- client.getNewAddress(addressType = AddressType.Legacy)
info <- client.getAddressInfo(addr)
} yield assert(info.timestamp.exists(_.dayOfYear == DateTime.now.dayOfYear))
}
// needs #360 to be merged
it should "be able to get the address info for a given Bech32 address" in {
for {
(client, _) <- clientsF
addr <- client.getNewAddress(AddressType.Bech32)
info <- client.getAddressInfo(addr)
} yield {
assert(info.address.networkParameters == RegTest)
assert(info.timestamp.exists(_.dayOfYear == DateTime.now.dayOfYear))
}
}
it should "be able to get the amount received by a label" in {
for {
(client, _) <- clientsF
address <- client.getNewAddress(usedLabel)
_ <- BitcoindRpcTestUtil
.fundBlockChainTransaction(client, address, Bitcoins(1.5))
amount <- client.getReceivedByLabel(usedLabel)
} yield assert(amount == Bitcoins(1.5))
}
it should "list all labels" in {
for {
(client, _) <- clientsF
_ <- client.listLabels()
} yield succeed
}
it should "list all labels with purposes" in {
clientsF.flatMap {
case (client, otherClient) =>
val sendLabel = "sendLabel"
val isImportDone = () =>
client.ping().map(_ => true).recover {
case exc if exc.getMessage.contains("rescanning") => false
case exc =>
logger.error(s"throwing $exc")
throw exc
}
def importTx(n: Int): Future[Unit] =
for {
address <- otherClient.getNewAddress
_ <- client.importAddress(address, sendLabel + n)
_ <- AsyncUtil.retryUntilSatisfiedF(isImportDone)
} yield ()
for {
_ <- importTx(0)
_ <- importTx(1)
receiveLabels <- client.listLabels(Some(LabelPurpose.Receive))
sendLabels <- client.listLabels(Some(LabelPurpose.Send))
} yield assert(receiveLabels != sendLabels)
}
}
it should "set labels" in {
val l = "setLabel"
val btc = Bitcoins(1)
for {
(client, otherClient) <- clientsF
addr <- client.getNewAddress
_ <- BitcoindRpcTestUtil.fundBlockChainTransaction(otherClient, addr, btc)
newestBlock <- otherClient.getBestBlockHash
_ <- AsyncUtil.retryUntilSatisfiedF(() =>
BitcoindRpcTestUtil.hasSeenBlock(client, newestBlock))
oldAmount <- client.getReceivedByLabel(l)
_ = assert(oldAmount == Bitcoins(0))
_ <- client.setLabel(addr, l)
newAmount <- client.getReceivedByLabel(l)
} yield assert(newAmount == btc)
}
it should "list amounts received by all labels" in {
for {
(client, otherClient) <- clientsF
addressWithLabel <- client.getNewAddress(usedLabel)
addressNoLabel <- client.getNewAddress
_ <- otherClient.sendToAddress(addressNoLabel, Bitcoins.one)
_ <- otherClient.sendToAddress(addressWithLabel, Bitcoins.one)
newBlock +: _ <- otherClient.generate(1)
_ <- AsyncUtil.retryUntilSatisfiedF(() =>
BitcoindRpcTestUtil.hasSeenBlock(client, newBlock))
list <- client.listReceivedByLabel()
} yield {
val receivedToUsedlabel = list.find(_.label == usedLabel)
assert(receivedToUsedlabel.isDefined)
assert(receivedToUsedlabel.get.amount > Bitcoins.zero)
val receivedDefaultLabel =
list
.find(_.label == "")
assert(receivedDefaultLabel.isDefined)
assert(receivedDefaultLabel.get.amount > Bitcoins.zero)
assert(list.forall(_.label != unusedLabel))
}
}
}

View File

@ -0,0 +1,178 @@
package org.bitcoins.rpc.v17
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script.ScriptSignature
import org.bitcoins.core.protocol.transaction.{
TransactionConstants,
TransactionInput,
TransactionOutPoint
}
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.rpc.client.v17.BitcoindV17RpcClient
import org.bitcoins.rpc.jsonmodels.{FinalizedPsbt, NonFinalizedPsbt}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import scala.concurrent.Future
class PsbtRpcTest extends BitcoindRpcTest {
lazy val clientsF: Future[
(BitcoindV17RpcClient, BitcoindV17RpcClient, BitcoindV17RpcClient)] = {
BitcoindRpcTestUtil.createNodeTripleV17(clientAccum)
}
behavior of "PsbtRpc"
// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Test_Vectors
it should "decode all the BIP174 example PSBTs" in {
val psbts = Vector(
"cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA",
"cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA",
"cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==",
"cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=",
"cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=",
"cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=",
"cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAA==" // this one is from Core
)
for {
(client, _, _) <- clientsF
_ <- Future.sequence(psbts.map(client.decodePsbt))
} yield succeed
}
it should "convert raw TXs to PSBTs, process them and then decode them" in {
for {
(client, _, _) <- clientsF
address <- client.getNewAddress
rawTx <- client.createRawTransaction(Vector.empty,
Map(address -> Bitcoins.one))
fundedRawTx <- client.fundRawTransaction(rawTx)
psbt <- client.convertToPsbt(fundedRawTx.hex)
processedPsbt <- client.walletProcessPsbt(psbt)
decoded <- client.decodePsbt(processedPsbt.psbt)
} yield {
assert(decoded.inputs.exists(_.nonWitnessUtxo.isDefined))
}
}
it should "finalize a simple PSBT" in {
for {
(client, _, _) <- clientsF
addr <- client.getNewAddress
txid <- BitcoindRpcTestUtil.fundBlockChainTransaction(client,
addr,
Bitcoins.one)
vout <- BitcoindRpcTestUtil.findOutput(client, txid, Bitcoins.one)
newAddr <- client.getNewAddress
psbt <- client.createPsbt(
Vector(TransactionInput.fromTxidAndVout(txid, vout)),
Map(newAddr -> Bitcoins(0.5)))
processed <- client.walletProcessPsbt(psbt)
finalized <- client.finalizePsbt(processed.psbt)
} yield
finalized match {
case _: FinalizedPsbt => succeed
case _: NonFinalizedPsbt => fail
}
}
// copies this test from Core: https://github.com/bitcoin/bitcoin/blob/master/test/functional/rpc_psbt.py#L158
it should "combine PSBTs from multiple sources" in {
for {
(client, otherClient, thirdClient) <- clientsF
// create outputs for transaction
clientAddr <- client.getNewAddress
otherClientAddr <- otherClient.getNewAddress
clientTxid <- thirdClient.sendToAddress(clientAddr, Bitcoins.one)
otherClientTxid <- thirdClient.sendToAddress(otherClientAddr,
Bitcoins.one)
_ <- BitcoindRpcTestUtil.generateAndSync(
Vector(thirdClient, client, otherClient))
rawClientTx <- client.getRawTransaction(clientTxid)
_ = assert(rawClientTx.confirmations.exists(_ > 0))
clientVout <- BitcoindRpcTestUtil.findOutput(client,
clientTxid,
Bitcoins.one)
otherClientVout <- BitcoindRpcTestUtil.findOutput(otherClient,
otherClientTxid,
Bitcoins.one)
// create a psbt spending outputs generated above
newAddr <- thirdClient.getNewAddress
psbt <- {
val inputs =
Vector(
TransactionInput
.fromTxidAndVout(clientTxid, clientVout),
TransactionInput.fromTxidAndVout(otherClientTxid, otherClientVout)
)
thirdClient.createPsbt(inputs, Map(newAddr -> Bitcoins(1.5)))
}
// Update psbts, should only have data for one input and not the other
clientProcessedPsbt <- client.walletProcessPsbt(psbt).map(_.psbt)
otherClientProcessedPsbt <- otherClient
.walletProcessPsbt(psbt)
.map(_.psbt)
// Combine and finalize the psbts
combined <- thirdClient.combinePsbt(
Vector(clientProcessedPsbt, otherClientProcessedPsbt))
finalized <- thirdClient.finalizePsbt(combined)
} yield {
finalized match {
case _: FinalizedPsbt => succeed
case _: NonFinalizedPsbt => fail
}
}
}
it should "create a PSBT and then decode it" in {
for {
(client, _, _) <- clientsF
address <- client.getNewAddress
input <- client.listUnspent.map(_.filter(_.spendable).head)
psbt <- {
val outpoint =
TransactionOutPoint(input.txid.flip, UInt32(input.vout))
val ourInput = TransactionInput(outpoint,
ScriptSignature.empty,
TransactionConstants.sequence)
client.createPsbt(
Vector(ourInput),
Map(address -> Bitcoins(input.amount.toBigDecimal / 2)))
}
_ <- client.decodePsbt(psbt)
} yield {
succeed
}
}
it should "create a funded wallet PSBT and then decode it" in {
for {
(client, _, _) <- clientsF
address <- client.getNewAddress
input <- client.listUnspent.map(_.filter(_.spendable).head)
psbt <- {
val outpoint = TransactionOutPoint(input.txid.flip, UInt32(input.vout))
val ourInput = TransactionInput(outpoint,
ScriptSignature.empty,
TransactionConstants.sequence)
client.walletCreateFundedPsbt(
Vector(ourInput),
Map(address -> Bitcoins(input.amount.toBigDecimal / 2)))
}
_ <- client.decodePsbt(psbt.psbt)
} yield {
succeed
}
}
}

View File

@ -2,4 +2,57 @@
> Note: `bitcoin-s-bitcoind-rpc` requires you to have `bitcoind` (Bitcoin Core daemon) installed. Grab this at [bitcoincore.org](https://bitcoincore.org/en/download/)
TODO
## Usage
The Bitcoin Core RPC client in Bitcoin-S currently supports the Bitcoin Core 0.16 and 0.17
version lines. It can be set up to work with both local and remote Bitcoin Core servers.
### Basic example
```scala
import org.bitcoins.rpc
import rpc.client.common._
import rpc.config.BitcoindInstance
import akka.actor.ActorSystem
// data directory defaults to ~/.bitcoin on Linux and
// ~/Library/Application Support/Bitcoin on macOS
val bitcoindInstance = BitcoindInstance.fromDatadir()
// alternative:
import java.io.File
val dataDir = new File("/my/bitcoin/data/dir")
val otherInstance = BitcoindInstance.fromDatadir(dataDir)
implicit val actorSystem: ActorSystem = ActorSystem.create()
val client = new BitcoindRpcClient(bitcoindInstance)
for {
_ <- client.start()
balance <- client.getBalance
} yield balance
```
### Advanced example
TODO: How to connect to remote bitcoind
## Testing
To test the Bitcoin-S RPC project you need both version 0.16 and 0.17 of Bitcoin Core. A list of current and previous releases can be found [here](https://bitcoincore.org/en/releases/).
You then need to set environment variables to indicate where Bitcoin-S can find the different versions:
```bash
$ export BITCOIND_V16_PATH=/path/to/v16/bitcoind
$ export BITCOIND_V17_PATH=/path/to/v17/bitcoind
```
If you just run tests testing common functionality it's enough to have either version 0.16 or 0.17 on your `PATH`.
To run all RPC related tests:
```bash
$ bash sbt bitcoindRpcTest/test
```

View File

@ -0,0 +1,54 @@
package org.bitcoins.rpc.client.common
import akka.actor.ActorSystem
import org.bitcoins.rpc.config.BitcoindInstance
/**
* This class is not guaranteed to be compatible with any particular
* version of Bitcoin Core. It implements RPC calls that are similar
* across different versions. If you need RPC calls specific to a
* version, check out
* [[org.bitcoins.rpc.client.v16.BitcoindV16RpcClient BitcoindV16RpcClient]]
* or
* [[org.bitcoins.rpc.client.v17.BitcoindV17RpcClient BitcoindV17RpcClient]].
*/
class BitcoindRpcClient(val instance: BitcoindInstance)(
implicit
override val system: ActorSystem)
extends Client
with BlockchainRpc
with MessageRpc
with MempoolRpc
with MiningRpc
with MultisigRpc
with NodeRpc
with P2PRpc
with RawTransactionRpc
with TransactionRpc
with UTXORpc
with WalletRpc
with UtilRpc {
override def version: BitcoindVersion = BitcoindVersion.Unknown
require(version == BitcoindVersion.Unknown || version == instance.getVersion,
s"bitcoind version must be $version, got ${instance.getVersion}")
}
sealed trait BitcoindVersion
object BitcoindVersion {
case object V16 extends BitcoindVersion {
override def toString: String = "v0.16"
}
case object V17 extends BitcoindVersion {
override def toString: String = "v0.17"
}
case object Unknown extends BitcoindVersion {
override def toString: String = "Unknown"
}
}

View File

@ -0,0 +1,241 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader}
import org.bitcoins.rpc.jsonmodels._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsBoolean, JsNumber, JsString}
import scala.concurrent.Future
/**
* RPC calls related to querying the state of the blockchain
*/
trait BlockchainRpc { self: Client =>
def getBestBlockHash: Future[DoubleSha256DigestBE] = {
bitcoindCall[DoubleSha256DigestBE]("getbestblockhash")
}
def getBlock(headerHash: DoubleSha256DigestBE): Future[GetBlockResult] = {
val isJsonObject = JsNumber(1)
bitcoindCall[GetBlockResult]("getblock",
List(JsString(headerHash.hex), isJsonObject))
}
def getBlock(headerHash: DoubleSha256Digest): Future[GetBlockResult] = {
getBlock(headerHash.flip)
}
def getBlockChainInfo: Future[GetBlockChainInfoResult] = {
bitcoindCall[GetBlockChainInfoResult]("getblockchaininfo")
}
def getBlockCount: Future[Int] = {
bitcoindCall[Int]("getblockcount")
}
def getBlockHash(height: Int): Future[DoubleSha256DigestBE] = {
bitcoindCall[DoubleSha256DigestBE]("getblockhash", List(JsNumber(height)))
}
def getBlockHeader(
headerHash: DoubleSha256DigestBE): Future[GetBlockHeaderResult] = {
bitcoindCall[GetBlockHeaderResult](
"getblockheader",
List(JsString(headerHash.hex), JsBoolean(true)))
}
def getBlockHeader(
headerHash: DoubleSha256Digest): Future[GetBlockHeaderResult] = {
getBlockHeader(headerHash.flip)
}
def getBlockHeaderRaw(
headerHash: DoubleSha256DigestBE): Future[BlockHeader] = {
bitcoindCall[BlockHeader]("getblockheader",
List(JsString(headerHash.hex), JsBoolean(false)))
}
def getBlockHeaderRaw(headerHash: DoubleSha256Digest): Future[BlockHeader] = {
getBlockHeaderRaw(headerHash.flip)
}
def getBlockRaw(headerHash: DoubleSha256DigestBE): Future[Block] = {
bitcoindCall[Block]("getblock", List(JsString(headerHash.hex), JsNumber(0)))
}
def getBlockRaw(headerHash: DoubleSha256Digest): Future[Block] = {
getBlockRaw(headerHash.flip)
}
def getBlockWithTransactions(headerHash: DoubleSha256DigestBE): Future[
GetBlockWithTransactionsResult] = {
val isVerboseJsonObject = JsNumber(2)
bitcoindCall[GetBlockWithTransactionsResult](
"getblock",
List(JsString(headerHash.hex), isVerboseJsonObject))
}
def getBlockWithTransactions(headerHash: DoubleSha256Digest): Future[
GetBlockWithTransactionsResult] = {
getBlockWithTransactions(headerHash.flip)
}
def getChainTips: Future[Vector[ChainTip]] = {
bitcoindCall[Vector[ChainTip]]("getchaintips")
}
def getChainTxStats: Future[GetChainTxStatsResult] =
getChainTxStats(None, None)
private def getChainTxStats(
blocks: Option[Int],
blockHash: Option[DoubleSha256DigestBE]): Future[GetChainTxStatsResult] = {
val params =
if (blocks.isEmpty) {
List.empty
} else if (blockHash.isEmpty) {
List(JsNumber(blocks.get))
} else {
List(JsNumber(blocks.get), JsString(blockHash.get.hex))
}
bitcoindCall[GetChainTxStatsResult]("getchaintxstats", params)
}
def getChainTxStats(blocks: Int): Future[GetChainTxStatsResult] =
getChainTxStats(Some(blocks), None)
def getChainTxStats(
blocks: Int,
blockHash: DoubleSha256DigestBE): Future[GetChainTxStatsResult] =
getChainTxStats(Some(blocks), Some(blockHash))
def getChainTxStats(
blocks: Int,
blockHash: DoubleSha256Digest): Future[GetChainTxStatsResult] =
getChainTxStats(Some(blocks), Some(blockHash.flip))
def getDifficulty: Future[BigDecimal] = {
bitcoindCall[BigDecimal]("getdifficulty")
}
def invalidateBlock(blockHash: DoubleSha256DigestBE): Future[Unit] = {
bitcoindCall[Unit]("invalidateblock", List(JsString(blockHash.hex)))
}
def invalidateBlock(blockHash: DoubleSha256Digest): Future[Unit] = {
invalidateBlock(blockHash.flip)
}
def listSinceBlock: Future[ListSinceBlockResult] = listSinceBlock(None)
def listSinceBlock(
headerHash: Option[DoubleSha256DigestBE] = None,
confirmations: Int = 1,
includeWatchOnly: Boolean = false): Future[ListSinceBlockResult] = {
val params =
if (headerHash.isEmpty) {
List.empty
} else {
List(JsString(headerHash.get.hex),
JsNumber(confirmations),
JsBoolean(includeWatchOnly))
}
bitcoindCall[ListSinceBlockResult]("listsinceblock", params)
}
def listSinceBlock(
headerHash: DoubleSha256DigestBE): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash))
def listSinceBlock(
headerHash: DoubleSha256DigestBE,
confirmations: Int): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash), confirmations)
def listSinceBlock(
headerHash: DoubleSha256DigestBE,
includeWatchOnly: Boolean): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash), includeWatchOnly = includeWatchOnly)
def listSinceBlock(
headerHash: DoubleSha256DigestBE,
confirmations: Int,
includeWatchOnly: Boolean): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash), confirmations, includeWatchOnly)
def listSinceBlock(
headerHash: DoubleSha256Digest): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash.flip))
def listSinceBlock(
headerHash: DoubleSha256Digest,
confirmations: Int): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash.flip), confirmations)
def listSinceBlock(
headerHash: DoubleSha256Digest,
includeWatchOnly: Boolean): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash.flip), includeWatchOnly = includeWatchOnly)
def listSinceBlock(
headerHash: DoubleSha256Digest,
confirmations: Int,
includeWatchOnly: Boolean): Future[ListSinceBlockResult] =
listSinceBlock(Some(headerHash.flip), confirmations, includeWatchOnly)
def listTransactions(
account: String = "*",
count: Int = 10,
skip: Int = 0,
includeWatchOnly: Boolean = false): Future[
Vector[ListTransactionsResult]] = {
bitcoindCall[Vector[ListTransactionsResult]](
"listtransactions",
List(JsString(account),
JsNumber(count),
JsNumber(skip),
JsBoolean(includeWatchOnly)))
}
def pruneBlockChain(height: Int): Future[Int] = {
bitcoindCall[Int]("pruneblockchain", List(JsNumber(height)))
}
def rescanBlockChain(): Future[RescanBlockChainResult] =
rescanBlockChain(None, None)
private def rescanBlockChain(
start: Option[Int],
stop: Option[Int]): Future[RescanBlockChainResult] = {
val params =
if (start.isEmpty) {
List.empty
} else if (stop.isEmpty) {
List(JsNumber(start.get))
} else {
List(JsNumber(start.get), JsNumber(stop.get))
}
bitcoindCall[RescanBlockChainResult]("rescanblockchain", params)
}
def rescanBlockChain(start: Int): Future[RescanBlockChainResult] =
rescanBlockChain(Some(start), None)
def rescanBlockChain(start: Int, stop: Int): Future[RescanBlockChainResult] =
rescanBlockChain(Some(start), Some(stop))
def preciousBlock(headerHash: DoubleSha256DigestBE): Future[Unit] = {
bitcoindCall[Unit]("preciousblock", List(JsString(headerHash.hex)))
}
def preciousBlock(headerHash: DoubleSha256Digest): Future[Unit] = {
preciousBlock(headerHash.flip)
}
def verifyChain(level: Int = 3, blocks: Int = 6): Future[Boolean] = {
bitcoindCall[Boolean]("verifychain",
List(JsNumber(level), JsNumber(blocks)))
}
}

View File

@ -0,0 +1,287 @@
package org.bitcoins.rpc.client.common
import java.io.File
import java.util.UUID
import akka.actor.ActorSystem
import akka.http.javadsl.model.headers.HttpCredentials
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.stream.{ActorMaterializer, StreamTcpException}
import akka.util.ByteString
import org.bitcoins.core.config.{MainNet, NetworkParameters, RegTest, TestNet3}
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.rpc.serializers.JsonSerializers._
import org.bitcoins.rpc.util.AsyncUtil
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}
/**
* This is the base trait for Bitcoin Core
* RPC clients. It defines no RPC calls
* except for the a ping. It contains functionality
* and utilities useful when working with an RPC
* client, like data directories, log files
* and whether or not the client is started.
*/
trait Client extends BitcoinSLogger {
def version: BitcoindVersion
protected val instance: BitcoindInstance
/**
* The log file of the Bitcoin Core daemon
*/
def logFile: File = {
val prefix = instance.network match {
case MainNet => ""
case TestNet3 => "/testnet"
case RegTest => "/regtest"
}
val datadir = instance.authCredentials.datadir
new File(datadir + prefix + "/debug.log")
}
protected implicit val system: ActorSystem
protected implicit val materializer: ActorMaterializer =
ActorMaterializer.create(system)
protected implicit val executor: ExecutionContext = system.getDispatcher
protected implicit val network: NetworkParameters = instance.network
/**
* This is here (and not in JsonWrriters)
* so that the implicit network val is accessible
*/
implicit object ECPrivateKeyWrites extends Writes[ECPrivateKey] {
override def writes(o: ECPrivateKey): JsValue = JsString(o.toWIF(network))
}
implicit val eCPrivateKeyWrites: Writes[ECPrivateKey] = ECPrivateKeyWrites
implicit val importMultiAddressWrites: Writes[RpcOpts.ImportMultiAddress] =
Json.writes[RpcOpts.ImportMultiAddress]
implicit val importMultiRequestWrites: Writes[RpcOpts.ImportMultiRequest] =
Json.writes[RpcOpts.ImportMultiRequest]
private val resultKey: String = "result"
private val errorKey: String = "error"
def getDaemon: BitcoindInstance = instance
/** Starts bitcoind on the local system.
* @return a future that completes when bitcoind is fully started.
* This future times out after 60 seconds if the client
* cannot be started
*/
def start(): Future[Unit] = {
if (version != BitcoindVersion.Unknown) {
val foundVersion = instance.getVersion
if (foundVersion != version) {
throw new RuntimeException(
s"Wrong version for bitcoind RPC client! Expected $version, got $foundVersion")
}
}
val binaryPath = instance.binary.getAbsolutePath
val cmd = List(binaryPath,
"-datadir=" + instance.authCredentials.datadir,
"-rpcport=" + instance.rpcUri.getPort,
"-port=" + instance.uri.getPort)
logger.debug(s"starting bitcoind")
val _ = Process(cmd).run()
def isStartedF: Future[Boolean] = {
val started: Promise[Boolean] = Promise()
val pingF = bitcoindCall[Unit]("ping", printError = false)
pingF.onComplete {
case Success(_) => started.success(true)
case Failure(_) => started.success(false)
}
started.future
}
val started = AsyncUtil.retryUntilSatisfiedF(() => isStartedF,
duration = 1.seconds,
maxTries = 60)
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)
}
}
started
}
/**
* Checks whether the underlying bitcoind daemon is running
*/
def isStartedF: Future[Boolean] = {
val request = buildRequest(instance, "ping", JsArray.empty)
val responseF = sendRequest(request)
val payloadF: Future[JsValue] = responseF.flatMap(getPayload)
// Ping successful if no error can be parsed from the payload
val parsedF = payloadF.map { payload =>
(payload \ errorKey).validate[RpcError] match {
case _: JsSuccess[RpcError] => false
case _: JsError => true
}
}
parsedF.recover {
case exc: StreamTcpException
if exc.getMessage.contains("Connection refused") =>
false
}
}
/**
* Checks whether the underlyind bitcoind daemon is stopped
*/
def isStoppedF: Future[Boolean] = {
isStartedF.map(started => !started)
}
// This RPC call is here to avoid circular trait depedency
def ping(): Future[Unit] = {
bitcoindCall[Unit]("ping")
}
protected def bitcoindCall[T](
command: String,
parameters: List[JsValue] = List.empty,
printError: Boolean = true)(
implicit
reader: Reads[T]): Future[T] = {
val request = buildRequest(instance, command, JsArray(parameters))
val responseF = sendRequest(request)
val payloadF: Future[JsValue] = responseF.flatMap(getPayload)
payloadF.map { payload =>
{
/**
* These lines are handy if you want to inspect what's being sent to and
* returned from bitcoind before it's parsed into a Scala type. However,
* there will sensitive material in some of those calls (private keys,
* XPUBs, balances, etc). It's therefore not a good idea to enable
* this logging in production.
*/
// logger.info(
// s"Command: $command ${parameters.map(_.toString).mkString(" ")}")
// logger.info(s"Payload: \n${Json.prettyPrint(payload)}")
parseResult(result = (payload \ resultKey).validate[T],
json = payload,
printError = printError,
command = command)
}
}
}
protected def buildRequest(
instance: BitcoindInstance,
methodName: String,
params: JsArray): HttpRequest = {
val uuid = UUID.randomUUID().toString
val m: Map[String, JsValue] = Map("method" -> JsString(methodName),
"params" -> params,
"id" -> JsString(uuid))
val jsObject = JsObject(m)
// Would toString work?
val uri = "http://" + instance.rpcUri.getHost + ":" + instance.rpcUri.getPort
val username = instance.authCredentials.username
val password = instance.authCredentials.password
HttpRequest(
method = HttpMethods.POST,
uri,
entity = HttpEntity(ContentTypes.`application/json`, jsObject.toString()))
.addCredentials(
HttpCredentials.createBasicHttpCredentials(username, password))
}
protected def sendRequest(req: HttpRequest): Future[HttpResponse] = {
Http(materializer.system).singleRequest(req)
}
protected def getPayload(response: HttpResponse): Future[JsValue] = {
val payloadF = response.entity.dataBytes.runFold(ByteString.empty)(_ ++ _)
payloadF.map { payload =>
Json.parse(payload.decodeString(ByteString.UTF_8))
}
}
// Should both logging and throwing be happening?
private def parseResult[T](
result: JsResult[T],
json: JsValue,
printError: Boolean,
command: String
): T = {
checkUnitError[T](result, json, printError)
result match {
case JsSuccess(value, _) => value
case res: JsError =>
(json \ errorKey).validate[RpcError] match {
case err: JsSuccess[RpcError] =>
if (printError) {
logger.error(s"Error ${err.value.code}: ${err.value.message}")
}
throw new RuntimeException(
s"Error $command ${err.value.code}: ${err.value.message}")
case _: JsError =>
val jsonResult = (json \ resultKey).get
val errString =
s"Error when parsing result of '$command': ${JsError.toJson(res).toString}!"
if (printError) logger.error(errString + s"JSON: $jsonResult")
throw new IllegalArgumentException(
s"Could not parse JsResult: $jsonResult! Error: $errString")
}
}
}
// Catches errors thrown by calls with Unit as the expected return type (which isn't handled by UnitReads)
private def checkUnitError[T](
result: JsResult[T],
json: JsValue,
printError: Boolean): Unit = {
if (result == JsSuccess(())) {
(json \ errorKey).validate[RpcError] match {
case err: JsSuccess[RpcError] =>
if (printError) {
logger.error(s"Error ${err.value.code}: ${err.value.message}")
}
throw new RuntimeException(
s"Error ${err.value.code}: ${err.value.message}")
case _: JsError =>
}
}
}
case class RpcError(code: Int, message: String)
implicit val rpcErrorReads: Reads[RpcError] = Json.reads[RpcError]
}

View File

@ -0,0 +1,101 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.rpc.jsonmodels.{
GetMemPoolEntryResult,
GetMemPoolInfoResult,
GetMemPoolResult
}
import org.bitcoins.rpc.serializers.JsonReaders._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsBoolean, JsString}
import scala.concurrent.Future
/**
* This trait defines RPC calls related to
* the mempool of a Bitcoin Core node. The
* mempool contains all unconfirmed transactions.
*/
trait MempoolRpc { selc: Client =>
def getMemPoolAncestors(
txid: DoubleSha256DigestBE): Future[Vector[DoubleSha256DigestBE]] = {
bitcoindCall[Vector[DoubleSha256DigestBE]](
"getmempoolancestors",
List(JsString(txid.hex), JsBoolean(false)))
}
def getMemPoolAncestors(
txid: DoubleSha256Digest): Future[Vector[DoubleSha256DigestBE]] = {
getMemPoolAncestors(txid.flip)
}
def getMemPoolAncestorsVerbose(txid: DoubleSha256DigestBE): Future[
Map[DoubleSha256DigestBE, GetMemPoolResult]] = {
bitcoindCall[Map[DoubleSha256DigestBE, GetMemPoolResult]](
"getmempoolancestors",
List(JsString(txid.hex), JsBoolean(true)))
}
def getMemPoolAncestorsVerbose(txid: DoubleSha256Digest): Future[
Map[DoubleSha256DigestBE, GetMemPoolResult]] = {
getMemPoolAncestorsVerbose(txid.flip)
}
def getMemPoolDescendants(
txid: DoubleSha256DigestBE): Future[Vector[DoubleSha256DigestBE]] = {
bitcoindCall[Vector[DoubleSha256DigestBE]](
"getmempooldescendants",
List(JsString(txid.hex), JsBoolean(false)))
}
def getMemPoolDescendants(
txid: DoubleSha256Digest): Future[Vector[DoubleSha256DigestBE]] = {
getMemPoolDescendants(txid.flip)
}
def getMemPoolDescendantsVerbose(txid: DoubleSha256DigestBE): Future[
Map[DoubleSha256DigestBE, GetMemPoolResult]] = {
bitcoindCall[Map[DoubleSha256DigestBE, GetMemPoolResult]](
"getmempooldescendants",
List(JsString(txid.hex), JsBoolean(true)))
}
def getMemPoolDescendantsVerbose(txid: DoubleSha256Digest): Future[
Map[DoubleSha256DigestBE, GetMemPoolResult]] = {
getMemPoolDescendantsVerbose(txid.flip)
}
def getMemPoolEntry(
txid: DoubleSha256DigestBE): Future[GetMemPoolEntryResult] = {
bitcoindCall[GetMemPoolEntryResult]("getmempoolentry",
List(JsString(txid.hex)))
}
def getMemPoolEntry(
txid: DoubleSha256Digest): Future[GetMemPoolEntryResult] = {
getMemPoolEntry(txid.flip)
}
def getMemPoolInfo: Future[GetMemPoolInfoResult] = {
bitcoindCall[GetMemPoolInfoResult]("getmempoolinfo")
}
def getRawMemPool: Future[Vector[DoubleSha256DigestBE]] = {
bitcoindCall[Vector[DoubleSha256DigestBE]]("getrawmempool",
List(JsBoolean(false)))
}
def getRawMemPoolWithTransactions: Future[
Map[DoubleSha256DigestBE, GetMemPoolResult]] = {
bitcoindCall[Map[DoubleSha256DigestBE, GetMemPoolResult]](
"getrawmempool",
List(JsBoolean(true)))
}
def saveMemPool(): Future[Unit] = {
bitcoindCall[Unit]("savemempool")
}
}

View File

@ -0,0 +1,35 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.protocol.P2PKHAddress
import play.api.libs.json.JsString
import scala.concurrent.Future
/**
* RPC calls related to the message signing functionality
* in bitcoind
*/
trait MessageRpc { self: Client =>
def signMessage(address: P2PKHAddress, message: String): Future[String] = {
bitcoindCall[String]("signmessage",
List(JsString(address.value), JsString(message)))
}
def signMessageWithPrivKey(
key: ECPrivateKey,
message: String): Future[String] = {
bitcoindCall[String]("signmessagewithprivkey",
List(JsString(key.toWIF(network)), JsString(message)))
}
def verifyMessage(
address: P2PKHAddress,
signature: String,
message: String): Future[Boolean] = {
bitcoindCall[Boolean](
"verifymessage",
List(JsString(address.value), JsString(signature), JsString(message)))
}
}

View File

@ -0,0 +1,71 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.rpc.jsonmodels.{GetBlockTemplateResult, GetMiningInfoResult}
import org.bitcoins.rpc.serializers.JsonReaders._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsNumber, JsString, Json}
import scala.concurrent.Future
/**
* RPC calls related to mining
*/
trait MiningRpc { self: Client =>
def generate(
blocks: Int,
maxTries: Int = 1000000): Future[Vector[DoubleSha256DigestBE]] = {
bitcoindCall[Vector[DoubleSha256DigestBE]](
"generate",
List(JsNumber(blocks), JsNumber(maxTries)))
}
def generateToAddress(
blocks: Int,
address: BitcoinAddress,
maxTries: Int = 1000000): Future[Vector[DoubleSha256DigestBE]] = {
bitcoindCall[Vector[DoubleSha256DigestBE]](
"generatetoaddress",
List(JsNumber(blocks), JsString(address.toString), JsNumber(maxTries)))
}
def getBlockTemplate(
request: Option[RpcOpts.BlockTemplateRequest] = None): Future[
GetBlockTemplateResult] = {
val params =
if (request.isEmpty) {
List.empty
} else {
List(Json.toJson(request.get))
}
bitcoindCall[GetBlockTemplateResult]("getblocktemplate", params)
}
def getNetworkHashPS(
blocks: Int = 120,
height: Int = -1): Future[BigDecimal] = {
bitcoindCall[BigDecimal]("getnetworkhashps",
List(JsNumber(blocks), JsNumber(height)))
}
def getMiningInfo: Future[GetMiningInfoResult] = {
bitcoindCall[GetMiningInfoResult]("getmininginfo")
}
def prioritiseTransaction(
txid: DoubleSha256DigestBE,
feeDelta: Satoshis): Future[Boolean] = {
bitcoindCall[Boolean](
"prioritisetransaction",
List(JsString(txid.hex), JsNumber(0), JsNumber(feeDelta.toLong)))
}
def prioritiseTransaction(
txid: DoubleSha256Digest,
feeDelta: Satoshis): Future[Boolean] = {
prioritiseTransaction(txid.flip, feeDelta)
}
}

View File

@ -0,0 +1,73 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.rpc.jsonmodels.MultiSigResult
import org.bitcoins.rpc.serializers.JsonSerializers._
import org.bitcoins.rpc.serializers.JsonWriters._
import play.api.libs.json.{JsArray, JsNumber, JsString, Json}
import scala.concurrent.Future
/**
* This trait defines RPC calls related to
* multisignature functionality in Bitcoin Core.
*
* @see [[https://en.bitcoin.it/wiki/Multisignature Bitcoin Wiki]]
* article on multisignature.
*/
trait MultisigRpc { self: Client =>
private def addMultiSigAddress(
minSignatures: Int,
keys: Vector[Either[ECPublicKey, P2PKHAddress]],
account: String = "",
addressType: Option[AddressType]): Future[MultiSigResult] = {
def keyToString(key: Either[ECPublicKey, P2PKHAddress]): JsString =
key match {
case Right(k) => JsString(k.value)
case Left(k) => JsString(k.hex)
}
val params =
List(JsNumber(minSignatures),
JsArray(keys.map(keyToString)),
JsString(account)) ++ addressType.map(Json.toJson(_)).toList
bitcoindCall[MultiSigResult]("addmultisigaddress", params)
}
def addMultiSigAddress(
minSignatures: Int,
keys: Vector[Either[ECPublicKey, P2PKHAddress]]): Future[MultiSigResult] =
addMultiSigAddress(minSignatures, keys, addressType = None)
def addMultiSigAddress(
minSignatures: Int,
keys: Vector[Either[ECPublicKey, P2PKHAddress]],
account: String): Future[MultiSigResult] =
addMultiSigAddress(minSignatures, keys, account, None)
def addMultiSigAddress(
minSignatures: Int,
keys: Vector[Either[ECPublicKey, P2PKHAddress]],
addressType: AddressType): Future[MultiSigResult] =
addMultiSigAddress(minSignatures, keys, addressType = Some(addressType))
def addMultiSigAddress(
minSignatures: Int,
keys: Vector[Either[ECPublicKey, P2PKHAddress]],
account: String,
addressType: AddressType): Future[MultiSigResult] =
addMultiSigAddress(minSignatures, keys, account, Some(addressType))
def createMultiSig(
minSignatures: Int,
keys: Vector[ECPublicKey]): Future[MultiSigResult] = {
bitcoindCall[MultiSigResult](
"createmultisig",
List(JsNumber(minSignatures), Json.toJson(keys.map(_.hex))))
}
}

View File

@ -0,0 +1,77 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.number.UInt32
import org.bitcoins.rpc.jsonmodels.GetMemoryInfoResult
import org.bitcoins.rpc.serializers.JsonReaders
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json._
import scala.concurrent.Future
/**
* RPC calls related to administration of a given node
*/
trait NodeRpc { self: Client =>
def abortRescan(): Future[Unit] = {
bitcoindCall[Unit]("abortrescan")
}
private def logging(
include: Option[Vector[String]],
exclude: Option[Vector[String]]): Future[Map[String, Boolean]] = {
val params = List(Json.toJson(include.getOrElse(Vector.empty)),
Json.toJson(exclude.getOrElse(Vector.empty)))
/**
* Bitcoin Core v0.16 returns a map of 1/0s,
* v0.17 returns proper booleans
*/
object IntOrBoolReads extends Reads[Boolean] {
override def reads(json: JsValue): JsResult[Boolean] =
json
.validate[Boolean]
.orElse(json.validate[Int].flatMap {
case 0 => JsSuccess(false)
case 1 => JsSuccess(true)
case other: Int => JsError(s"$other is not a boolean, 1 or 0")
})
}
object LoggingReads extends Reads[Map[String, Boolean]] {
override def reads(json: JsValue): JsResult[Map[String, Boolean]] =
JsonReaders.mapReads(json)(implicitly[Reads[String]], IntOrBoolReads)
}
// if we're just compiling for Scala 2.12 we could have converted the 20 lines
// above into a one-liner, but Play Json for 2.11 isn't quite clever enough
bitcoindCall[Map[String, Boolean]]("logging", params)(LoggingReads)
}
def logging: Future[Map[String, Boolean]] = logging(None, None)
def logging(
include: Vector[String] = Vector.empty,
exclude: Vector[String] = Vector.empty): Future[Map[String, Boolean]] = {
val inc = if (include.nonEmpty) Some(include) else None
val exc = if (exclude.nonEmpty) Some(exclude) else None
logging(inc, exc)
}
def uptime: Future[UInt32] = {
bitcoindCall[UInt32]("uptime")
}
def getMemoryInfo: Future[GetMemoryInfoResult] = {
bitcoindCall[GetMemoryInfoResult]("getmemoryinfo")
}
def help(rpcName: String = ""): Future[String] = {
bitcoindCall[String]("help", List(JsString(rpcName)))
}
def stop(): Future[String] = {
bitcoindCall[String]("stop")
}
}

View File

@ -0,0 +1,88 @@
package org.bitcoins.rpc.client.common
import java.net.URI
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.rpc.client.common.RpcOpts.{AddNodeArgument, SetBanCommand}
import org.bitcoins.rpc.jsonmodels._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsBoolean, JsNumber, JsString}
import scala.concurrent.Future
/**
* This trait defines functionality relating to how
* Bitcoin Core connects to and selects its network peers.
*/
trait P2PRpc { self: Client =>
def addNode(address: URI, command: AddNodeArgument): Future[Unit] = {
bitcoindCall[Unit](
"addnode",
List(JsString(address.getAuthority), JsString(command.toString)))
}
def clearBanned(): Future[Unit] = {
bitcoindCall[Unit]("clearbanned")
}
def disconnectNode(address: URI): Future[Unit] = {
bitcoindCall[Unit]("disconnectnode", List(JsString(address.getAuthority)))
}
def getAddedNodeInfo: Future[Vector[Node]] = getAddedNodeInfo(None)
private def getAddedNodeInfo(node: Option[URI]): Future[Vector[Node]] = {
val params =
if (node.isEmpty) {
List.empty
} else {
List(JsString(node.get.getAuthority))
}
bitcoindCall[Vector[Node]]("getaddednodeinfo", params)
}
def getAddedNodeInfo(node: URI): Future[Vector[Node]] =
getAddedNodeInfo(Some(node))
def getConnectionCount: Future[Int] = {
bitcoindCall[Int]("getconnectioncount")
}
def getNetworkInfo: Future[GetNetworkInfoResult] = {
bitcoindCall[GetNetworkInfoResult]("getnetworkinfo")
}
def getNetTotals: Future[GetNetTotalsResult] = {
bitcoindCall[GetNetTotalsResult]("getnettotals")
}
def getPeerInfo: Future[Vector[Peer]] = {
bitcoindCall[Vector[Peer]]("getpeerinfo")
}
def listBanned: Future[Vector[NodeBan]] = {
bitcoindCall[Vector[NodeBan]]("listbanned")
}
def setBan(
address: URI,
command: SetBanCommand,
banTime: Int = 86400,
absolute: Boolean = false): Future[Unit] = {
bitcoindCall[Unit]("setban",
List(JsString(address.getAuthority),
JsString(command.toString),
JsNumber(banTime),
JsBoolean(absolute)))
}
def setNetworkActive(activate: Boolean): Future[Unit] = {
bitcoindCall[Unit]("setnetworkactive", List(JsBoolean(activate)))
}
def submitBlock(block: Block): Future[Unit] = {
bitcoindCall[Unit]("submitblock", List(JsString(block.hex)))
}
}

View File

@ -0,0 +1,97 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionInput}
import org.bitcoins.rpc.jsonmodels.{
FundRawTransactionResult,
GetRawTransactionResult,
RpcTransaction
}
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json._
import scala.concurrent.Future
/**
* This trait defines RPC calls relating to interacting
* with raw transactions. This includes creation, decoding
* funding and sending.
*/
trait RawTransactionRpc { self: Client =>
def combineRawTransaction(txs: Vector[Transaction]): Future[Transaction] = {
bitcoindCall[Transaction]("combinerawtransaction", List(Json.toJson(txs)))
}
def createRawTransaction(
inputs: Vector[TransactionInput],
outputs: Map[BitcoinAddress, Bitcoins],
locktime: Int = 0): Future[Transaction] = {
bitcoindCall[Transaction](
"createrawtransaction",
List(Json.toJson(inputs), Json.toJson(outputs), JsNumber(locktime)))
}
def decodeRawTransaction(transaction: Transaction): Future[RpcTransaction] = {
bitcoindCall[RpcTransaction]("decoderawtransaction",
List(JsString(transaction.hex)))
}
def fundRawTransaction(
transaction: Transaction): Future[FundRawTransactionResult] =
fundRawTransaction(transaction, None)
private def fundRawTransaction(
transaction: Transaction,
options: Option[RpcOpts.FundRawTransactionOptions]): Future[
FundRawTransactionResult] = {
val params =
if (options.isEmpty) {
List(JsString(transaction.hex))
} else {
List(JsString(transaction.hex), Json.toJson(options.get))
}
bitcoindCall[FundRawTransactionResult]("fundrawtransaction", params)
}
def fundRawTransaction(
transaction: Transaction,
options: RpcOpts.FundRawTransactionOptions): Future[
FundRawTransactionResult] = fundRawTransaction(transaction, Some(options))
def getRawTransaction(
txid: DoubleSha256DigestBE,
blockhash: Option[DoubleSha256DigestBE] = None): Future[
GetRawTransactionResult] = {
val lastParam: List[JsString] = blockhash match {
case Some(hash) => JsString(hash.hex) :: Nil
case None => Nil
}
val params = List(JsString(txid.hex), JsBoolean(true)) ++ lastParam
bitcoindCall[GetRawTransactionResult]("getrawtransaction", params)
}
def getRawTransactionRaw(
txid: DoubleSha256DigestBE,
blockhash: Option[DoubleSha256DigestBE] = None): Future[Transaction] = {
val lastParam: List[JsString] = blockhash match {
case Some(hash) => JsString(hash.hex) :: Nil
case None => Nil
}
val params = List(JsString(txid.hex), JsBoolean(false)) ++ lastParam
bitcoindCall[Transaction]("getrawtransaction", params)
}
def sendRawTransaction(
transaction: Transaction,
allowHighFees: Boolean = false): Future[DoubleSha256DigestBE] = {
bitcoindCall[DoubleSha256DigestBE](
"sendrawtransaction",
List(JsString(transaction.hex), JsBoolean(allowHighFees)))
}
}

View File

@ -0,0 +1,164 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, ECPrivateKey}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.{ScriptPubKey, WitnessScriptPubKey}
import org.bitcoins.core.protocol.transaction.TransactionInput
import org.bitcoins.rpc.serializers.JsonWriters._
import play.api.libs.json.{Json, Writes}
object RpcOpts {
case class WalletCreateFundedPsbtOptions(
changeAddress: Option[BitcoinAddress] = None,
changePosition: Option[Int] = None,
changeType: Option[AddressType] = None,
includeWatching: Boolean = false,
lockUnspents: Boolean = false,
feeRate: Option[Bitcoins] = None,
subtractFeeFromOutputs: Option[Vector[Int]] = None,
replaceable: Boolean = false,
confTarget: Option[Int] = None,
estimateMode: FeeEstimationMode = FeeEstimationMode.Unset
)
case class FundRawTransactionOptions(
changeAddress: Option[BitcoinAddress] = None,
changePosition: Option[Int] = None,
includeWatching: Boolean = false,
lockUnspents: Boolean = false,
reverseChangeKey: Boolean = true,
feeRate: Option[Bitcoins] = None,
subtractFeeFromOutputs: Option[Vector[Int]])
sealed abstract class FeeEstimationMode
object FeeEstimationMode {
case object Unset extends FeeEstimationMode {
override def toString: String = "UNSET"
}
case object Ecnomical extends FeeEstimationMode {
override def toString: String = "ECONOMICAL"
}
case object Conservative extends FeeEstimationMode {
override def toString: String = "CONSERVATIVE"
}
}
sealed abstract class SetBanCommand
object SetBanCommand {
case object Add extends SetBanCommand {
override def toString: String = "add"
}
case object Remove extends SetBanCommand {
override def toString: String = "remove"
}
}
implicit val fundRawTransactionOptionsWrites: Writes[
FundRawTransactionOptions] = Json.writes[FundRawTransactionOptions]
case class SignRawTransactionOutputParameter(
txid: DoubleSha256DigestBE,
vout: Int,
scriptPubKey: ScriptPubKey,
redeemScript: Option[ScriptPubKey] = None,
witnessScript: Option[WitnessScriptPubKey] = None,
amount: Option[Bitcoins] = None)
implicit val signRawTransactionOutputParameterWrites: Writes[
SignRawTransactionOutputParameter] =
Json.writes[SignRawTransactionOutputParameter]
object SignRawTransactionOutputParameter {
def fromTransactionInput(
transactionInput: TransactionInput,
scriptPubKey: ScriptPubKey,
redeemScript: Option[ScriptPubKey] = None,
witnessScript: Option[WitnessScriptPubKey] = None,
amount: Option[Bitcoins] = None): SignRawTransactionOutputParameter = {
SignRawTransactionOutputParameter(
txid = transactionInput.previousOutput.txIdBE,
vout = transactionInput.previousOutput.vout.toInt,
scriptPubKey = scriptPubKey,
redeemScript = redeemScript,
witnessScript = witnessScript,
amount = amount
)
}
}
case class ImportMultiRequest(
scriptPubKey: ImportMultiAddress,
timestamp: UInt32,
redeemscript: Option[ScriptPubKey] = None,
pubkeys: Option[Vector[ScriptPubKey]] = None,
keys: Option[Vector[ECPrivateKey]] = None,
internal: Option[Boolean] = None,
watchonly: Option[Boolean] = None,
label: Option[String] = None)
case class ImportMultiAddress(address: BitcoinAddress)
case class LockUnspentOutputParameter(txid: DoubleSha256DigestBE, vout: Int)
implicit val lockUnspentParameterWrites: Writes[LockUnspentOutputParameter] =
Json.writes[LockUnspentOutputParameter]
sealed trait AddNodeArgument
object AddNodeArgument {
case object Add extends AddNodeArgument {
override def toString: String = "add"
}
case object Remove extends AddNodeArgument {
override def toString: String = "remove"
}
case object OneTry extends AddNodeArgument {
override def toString: String = "onetry"
}
}
sealed trait AddressType
object AddressType {
case object Legacy extends AddressType {
override def toString: String = "legacy"
}
case object P2SHSegwit extends AddressType {
override def toString: String = "p2sh-segwit"
}
case object Bech32 extends AddressType {
override def toString: String = "bech32"
}
}
sealed trait LabelPurpose
object LabelPurpose {
case object Send extends LabelPurpose {
override def toString: String = "send"
}
case object Receive extends LabelPurpose {
override def toString: String = "receive"
}
}
case class BlockTemplateRequest(
mode: String,
capabilities: Vector[String],
rules: Vector[String])
implicit val blockTemplateRequest: Writes[BlockTemplateRequest] =
Json.writes[BlockTemplateRequest]
}

View File

@ -0,0 +1,164 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.MerkleBlock
import org.bitcoins.rpc.client.common.RpcOpts.{AddressType, FeeEstimationMode}
import org.bitcoins.rpc.jsonmodels._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json._
import scala.concurrent.Future
/**
* This trait defines RPC calls related to transactions
* in Bitcoin Core. These RPC calls generally provide a
* higher level of abstraction than the ones found in
* [[org.bitcoins.rpc.client.common.RawTransactionRpc RawTransactionRpc]].
*/
trait TransactionRpc { self: Client =>
def abandonTransaction(txid: DoubleSha256DigestBE): Future[Unit] = {
bitcoindCall[Unit]("abandontransaction", List(JsString(txid.hex)))
}
def abandonTransaction(txid: DoubleSha256Digest): Future[Unit] = {
abandonTransaction(txid.flip)
}
def bumpFee(
txid: DoubleSha256DigestBE,
confTarget: Int = 6,
totalFee: Option[Satoshis] = None,
replaceable: Boolean = true,
estimateMode: String = "UNSET"): Future[BumpFeeResult] = {
val optionsNoFee =
Map("confTarget" -> JsNumber(confTarget),
"replaceable" -> JsBoolean(replaceable),
"estimate_mode" -> JsString(estimateMode))
val options = totalFee match {
case Some(fee) =>
optionsNoFee + ("totalFee" -> JsNumber(fee.toBigDecimal))
case None => optionsNoFee
}
bitcoindCall[BumpFeeResult]("bumpfee",
List(JsString(txid.hex), JsObject(options)))
}
def bumpFee(
txid: DoubleSha256Digest,
confTarget: Int,
totalFee: Option[Satoshis],
replaceable: Boolean,
estimateMode: String): Future[BumpFeeResult] = {
bumpFee(txid.flip, confTarget, totalFee, replaceable, estimateMode)
}
// Needs manual testing!
def estimateSmartFee(
blocks: Int,
mode: FeeEstimationMode = FeeEstimationMode.Ecnomical): Future[
EstimateSmartFeeResult] = {
bitcoindCall[EstimateSmartFeeResult](
"estimatesmartfee",
List(JsNumber(blocks), JsString(mode.toString)))
}
def getTransaction(
txid: DoubleSha256DigestBE,
watchOnly: Boolean = false): Future[GetTransactionResult] = {
bitcoindCall[GetTransactionResult](
"gettransaction",
List(JsString(txid.hex), JsBoolean(watchOnly)))
}
def getTxOut(
txid: DoubleSha256DigestBE,
vout: Int,
includeMemPool: Boolean = true): Future[GetTxOutResult] = {
bitcoindCall[GetTxOutResult](
"gettxout",
List(JsString(txid.hex), JsNumber(vout), JsBoolean(includeMemPool)))
}
private def getTxOutProof(
txids: Vector[DoubleSha256DigestBE],
headerHash: Option[DoubleSha256DigestBE]): Future[MerkleBlock] = {
val params = {
val hashes = JsArray(txids.map(hash => JsString(hash.hex)))
if (headerHash.isEmpty) {
List(hashes)
} else {
List(hashes, JsString(headerHash.get.hex))
}
}
bitcoindCall[MerkleBlock]("gettxoutproof", params)
}
def getTxOutProof(txids: Vector[DoubleSha256DigestBE]): Future[MerkleBlock] =
getTxOutProof(txids, None)
def getTxOutProof(
txids: Vector[DoubleSha256Digest],
headerHash: DoubleSha256Digest): Future[MerkleBlock] =
getTxOutProof(txids.map(_.flip), Some(headerHash.flip))
def getTxOutProof(
txids: Vector[DoubleSha256DigestBE],
headerHash: DoubleSha256DigestBE): Future[MerkleBlock] =
getTxOutProof(txids, Some(headerHash))
def verifyTxOutProof(
proof: MerkleBlock): Future[Vector[DoubleSha256DigestBE]] = {
bitcoindCall[Vector[DoubleSha256DigestBE]]("verifytxoutproof",
List(JsString(proof.hex)))
}
def getTxOutSetInfo: Future[GetTxOutSetInfoResult] = {
bitcoindCall[GetTxOutSetInfoResult]("gettxoutsetinfo")
}
def getRawChangeAddress: Future[BitcoinAddress] = getRawChangeAddress(None)
def getRawChangeAddress(addressType: AddressType): Future[BitcoinAddress] =
getRawChangeAddress(Some(addressType))
private def getRawChangeAddress(
addressType: Option[AddressType]): Future[BitcoinAddress] = {
bitcoindCall[BitcoinAddress]("getrawchangeaddress",
addressType.map(Json.toJson(_)).toList)
}
def sendMany(
amounts: Map[BitcoinAddress, Bitcoins],
minconf: Int = 1,
comment: String = "",
subtractFeeFrom: Vector[BitcoinAddress] = Vector.empty): Future[
DoubleSha256DigestBE] = {
bitcoindCall[DoubleSha256DigestBE]("sendmany",
List(JsString(""),
Json.toJson(amounts),
JsNumber(minconf),
JsString(comment),
Json.toJson(subtractFeeFrom)))
}
def sendToAddress(
address: BitcoinAddress,
amount: Bitcoins,
localComment: String = "",
toComment: String = "",
subractFeeFromAmount: Boolean = false): Future[DoubleSha256DigestBE] = {
bitcoindCall[DoubleSha256DigestBE](
"sendtoaddress",
List(JsString(address.toString),
JsNumber(amount.toBigDecimal),
JsString(localComment),
JsString(toComment),
JsBoolean(subractFeeFromAmount))
)
}
}

View File

@ -0,0 +1,59 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import org.bitcoins.rpc.jsonmodels.UnspentOutput
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsBoolean, JsNumber, Json}
import scala.concurrent.Future
/**
* This trait defines functionality related to
* UTXOs (unspent transaction outputs).
*
* @see [[https://bitcoin.org/en/developer-guide#term-utxo Bitcoin.org]]
* developer guide article on UTXOs
*/
trait UTXORpc { self: Client =>
def listLockUnspent: Future[Vector[TransactionOutPoint]] = {
bitcoindCall[Vector[TransactionOutPoint]]("listlockunspent")
}
def listUnspent: Future[Vector[UnspentOutput]] = listUnspent(addresses = None)
def listUnspent(
minConfirmations: Int,
maxConfirmations: Int): Future[Vector[UnspentOutput]] =
listUnspent(minConfirmations, maxConfirmations, None)
def listUnspent(
addresses: Vector[BitcoinAddress]): Future[Vector[UnspentOutput]] =
listUnspent(addresses = addresses)
def listUnspent(
minConfirmations: Int,
maxConfirmations: Int,
addresses: Vector[BitcoinAddress]): Future[Vector[UnspentOutput]] =
listUnspent(minConfirmations, maxConfirmations, Some(addresses))
private def listUnspent(
minConfirmations: Int = 1,
maxConfirmations: Int = 9999999,
addresses: Option[Vector[BitcoinAddress]]): Future[
Vector[UnspentOutput]] = {
val params =
List(JsNumber(minConfirmations), JsNumber(maxConfirmations)) ++
addresses.map(Json.toJson(_)).toList
bitcoindCall[Vector[UnspentOutput]]("listunspent", params)
}
def lockUnspent(
unlock: Boolean,
outputs: Vector[RpcOpts.LockUnspentOutputParameter]): Future[Boolean] = {
bitcoindCall[Boolean]("lockunspent",
List(JsBoolean(unlock), Json.toJson(outputs)))
}
}

View File

@ -0,0 +1,29 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.rpc.jsonmodels.{
DecodeScriptResult,
ValidateAddressResult,
ValidateAddressResultImpl
}
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsString, Json}
import scala.concurrent.Future
/*
* Utility RPC calls
*/
trait UtilRpc { self: Client =>
def validateAddress(
address: BitcoinAddress): Future[ValidateAddressResult] = {
bitcoindCall[ValidateAddressResultImpl]("validateaddress",
List(JsString(address.toString)))
}
def decodeScript(script: ScriptPubKey): Future[DecodeScriptResult] = {
bitcoindCall[DecodeScriptResult]("decodescript", List(Json.toJson(script)))
}
}

View File

@ -0,0 +1,187 @@
package org.bitcoins.rpc.client.common
import org.bitcoins.core.crypto.{
DoubleSha256Digest,
DoubleSha256DigestBE,
ECPrivateKey,
ECPublicKey
}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.MerkleBlock
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.rpc.jsonmodels._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json._
import scala.concurrent.Future
/**
* RPC calls related to wallet management
* functionality in bitcoind
*/
trait WalletRpc { self: Client =>
def backupWallet(destination: String): Future[Unit] = {
bitcoindCall[Unit]("backupwallet", List(JsString(destination)))
}
def dumpPrivKey(address: BitcoinAddress): Future[ECPrivateKey] = {
bitcoindCall[String]("dumpprivkey", List(JsString(address.value)))
.map(ECPrivateKey.fromWIFToPrivateKey)
}
def dumpWallet(filePath: String): Future[DumpWalletResult] = {
bitcoindCall[DumpWalletResult]("dumpwallet", List(JsString(filePath)))
}
def encryptWallet(passphrase: String): Future[String] = {
bitcoindCall[String]("encryptwallet", List(JsString(passphrase)))
}
def getBalance: Future[Bitcoins] = {
bitcoindCall[Bitcoins]("getbalance")
}
def getReceivedByAddress(
address: BitcoinAddress,
minConfirmations: Int = 1): Future[Bitcoins] = {
bitcoindCall[Bitcoins](
"getreceivedbyaddress",
List(JsString(address.toString), JsNumber(minConfirmations)))
}
def getUnconfirmedBalance: Future[Bitcoins] = {
bitcoindCall[Bitcoins]("getunconfirmedbalance")
}
def importAddress(
address: BitcoinAddress,
account: String = "",
rescan: Boolean = true,
p2sh: Boolean = false): Future[Unit] = {
bitcoindCall[Unit]("importaddress",
List(JsString(address.value),
JsString(account),
JsBoolean(rescan),
JsBoolean(p2sh)))
}
private def getNewAddressInternal(
accountOrLabel: String = "",
addressType: Option[AddressType]): Future[BitcoinAddress] = {
val params =
List(JsString(accountOrLabel)) ++ addressType.map(Json.toJson(_)).toList
bitcoindCall[BitcoinAddress]("getnewaddress", params)
}
def getNewAddress: Future[BitcoinAddress] =
getNewAddressInternal(addressType = None)
def getNewAddress(addressType: AddressType): Future[BitcoinAddress] =
getNewAddressInternal(addressType = Some(addressType))
def getNewAddress(accountOrLabel: String): Future[BitcoinAddress] =
getNewAddressInternal(accountOrLabel, None)
def getNewAddress(
accountOrLabel: String,
addressType: AddressType): Future[BitcoinAddress] =
getNewAddressInternal(accountOrLabel, Some(addressType))
def getWalletInfo: Future[GetWalletInfoResult] = {
bitcoindCall[GetWalletInfoResult]("getwalletinfo")
}
def keyPoolRefill(keyPoolSize: Int = 100): Future[Unit] = {
bitcoindCall[Unit]("keypoolrefill", List(JsNumber(keyPoolSize)))
}
def importPubKey(
pubKey: ECPublicKey,
label: String = "",
rescan: Boolean = true): Future[Unit] = {
bitcoindCall[Unit](
"importpubkey",
List(JsString(pubKey.hex), JsString(label), JsBoolean(rescan)))
}
def importPrivKey(
key: ECPrivateKey,
account: String = "",
rescan: Boolean = true): Future[Unit] = {
bitcoindCall[Unit](
"importprivkey",
List(JsString(key.toWIF(network)), JsString(account), JsBoolean(rescan)))
}
def importMulti(
requests: Vector[RpcOpts.ImportMultiRequest],
rescan: Boolean = true): Future[Vector[ImportMultiResult]] = {
bitcoindCall[Vector[ImportMultiResult]](
"importmulti",
List(Json.toJson(requests), JsObject(Map("rescan" -> JsBoolean(rescan)))))
}
def importPrunedFunds(
transaction: Transaction,
txOutProof: MerkleBlock): Future[Unit] = {
bitcoindCall[Unit](
"importprunedfunds",
List(JsString(transaction.hex), JsString(txOutProof.hex)))
}
def removePrunedFunds(txid: DoubleSha256DigestBE): Future[Unit] = {
bitcoindCall[Unit]("removeprunedfunds", List(JsString(txid.hex)))
}
def removePrunedFunds(txid: DoubleSha256Digest): Future[Unit] = {
removePrunedFunds(txid.flip)
}
def importWallet(filePath: String): Future[Unit] = {
bitcoindCall[Unit]("importwallet", List(JsString(filePath)))
}
def listAddressGroupings: Future[Vector[Vector[RpcAddress]]] = {
bitcoindCall[Vector[Vector[RpcAddress]]]("listaddressgroupings")
}
def listReceivedByAddress(
confirmations: Int = 1,
includeEmpty: Boolean = false,
includeWatchOnly: Boolean = false): Future[Vector[ReceivedAddress]] = {
bitcoindCall[Vector[ReceivedAddress]]("listreceivedbyaddress",
List(JsNumber(confirmations),
JsBoolean(includeEmpty),
JsBoolean(includeWatchOnly)))
}
def listWallets: Future[Vector[String]] = {
bitcoindCall[Vector[String]]("listwallets")
}
// TODO: Should be BitcoinFeeUnit
def setTxFee(feePerKB: Bitcoins): Future[Boolean] = {
bitcoindCall[Boolean]("settxfee", List(JsNumber(feePerKB.toBigDecimal)))
}
def walletLock(): Future[Unit] = {
bitcoindCall[Unit]("walletlock")
}
def walletPassphrase(passphrase: String, seconds: Int): Future[Unit] = {
bitcoindCall[Unit]("walletpassphrase",
List(JsString(passphrase), JsNumber(seconds)))
}
def walletPassphraseChange(
currentPassphrase: String,
newPassphrase: String): Future[Unit] = {
bitcoindCall[Unit](
"walletpassphrasechange",
List(JsString(currentPassphrase), JsString(newPassphrase)))
}
}

View File

@ -0,0 +1,83 @@
package org.bitcoins.rpc.client.v16
import akka.actor.ActorSystem
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.rpc.client.common.{
BitcoindRpcClient,
BitcoindVersion,
RpcOpts
}
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.rpc.jsonmodels.SignRawTransactionResult
import org.bitcoins.rpc.serializers.JsonSerializers._
import org.bitcoins.rpc.serializers.JsonWriters._
import play.api.libs.json._
import scala.concurrent.Future
import scala.util.Try
/**
* This class is compatible with version 0.16 of Bitcoin Core.
*/
class BitcoindV16RpcClient(override val instance: BitcoindInstance)(
implicit
actorSystem: ActorSystem)
extends BitcoindRpcClient(instance)
with V16AccountRpc
with V16SendRpc {
override def version: BitcoindVersion = BitcoindVersion.V16
def signRawTransaction(
transaction: Transaction): Future[SignRawTransactionResult] =
signRawTransaction(transaction, None, None, None)
private def signRawTransaction(
transaction: Transaction,
utxoDeps: Option[Vector[RpcOpts.SignRawTransactionOutputParameter]],
keys: Option[Vector[ECPrivateKey]],
sigHash: Option[HashType]): Future[SignRawTransactionResult] = {
val utxos: JsValue = utxoDeps.map(Json.toJson(_)).getOrElse(JsNull)
val jsonKeys: JsValue = keys.map(Json.toJson(_)).getOrElse(JsNull)
val params =
List(JsString(transaction.hex),
utxos,
jsonKeys,
Json.toJson(sigHash.getOrElse(HashType.sigHashAll)))
bitcoindCall[SignRawTransactionResult]("signrawtransaction", params)
}
def signRawTransaction(
transaction: Transaction,
utxoDeps: Vector[RpcOpts.SignRawTransactionOutputParameter]): Future[
SignRawTransactionResult] =
signRawTransaction(transaction, Some(utxoDeps), None, None)
def signRawTransaction(
transaction: Transaction,
utxoDeps: Vector[RpcOpts.SignRawTransactionOutputParameter],
keys: Vector[ECPrivateKey]): Future[SignRawTransactionResult] =
signRawTransaction(transaction, Some(utxoDeps), Some(keys), None)
def signRawTransaction(
transaction: Transaction,
utxoDeps: Vector[RpcOpts.SignRawTransactionOutputParameter],
keys: Vector[ECPrivateKey],
sigHash: HashType): Future[SignRawTransactionResult] =
signRawTransaction(transaction, Some(utxoDeps), Some(keys), Some(sigHash))
}
object BitcoindV16RpcClient {
def fromUnknownVersion(rpcClient: BitcoindRpcClient)(
implicit actorSystem: ActorSystem): Try[BitcoindV16RpcClient] =
Try {
new BitcoindV16RpcClient(rpcClient.instance)
}
}

View File

@ -0,0 +1,63 @@
package org.bitcoins.rpc.client.v16
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.rpc.client.common.Client
import org.bitcoins.rpc.jsonmodels.ReceivedAccount
import org.bitcoins.rpc.serializers.JsonReaders._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsBoolean, JsNumber, JsString}
import scala.concurrent.Future
/**
* Bitcoin Core prior to version 0.17 had the concept of
* accounts. This has later been removed, and replaced
* with a label system, as well as functionality for
* having several distinct wallets active at the same time.
*/
trait V16AccountRpc { self: Client =>
def getAccountAddress(account: String): Future[BitcoinAddress] = {
bitcoindCall[BitcoinAddress]("getaccountaddress", List(JsString(account)))
}
def getReceivedByAccount(
account: String,
confirmations: Int = 1): Future[Bitcoins] = {
bitcoindCall[Bitcoins]("getreceivedbyaccount",
List(JsString(account), JsNumber(confirmations)))
}
def getAccount(address: BitcoinAddress): Future[String] = {
bitcoindCall[String]("getaccount", List(JsString(address.value)))
}
def getAddressesByAccount(account: String): Future[Vector[BitcoinAddress]] = {
bitcoindCall[Vector[BitcoinAddress]]("getaddressesbyaccount",
List(JsString(account)))
}
def listAccounts(
confirmations: Int = 1,
includeWatchOnly: Boolean = false): Future[Map[String, Bitcoins]] = {
bitcoindCall[Map[String, Bitcoins]](
"listaccounts",
List(JsNumber(confirmations), JsBoolean(includeWatchOnly)))
}
def setAccount(address: BitcoinAddress, account: String): Future[Unit] = {
bitcoindCall[Unit]("setaccount",
List(JsString(address.value), JsString(account)))
}
def listReceivedByAccount(
confirmations: Int = 1,
includeEmpty: Boolean = false,
includeWatchOnly: Boolean = false): Future[Vector[ReceivedAccount]] = {
bitcoindCall[Vector[ReceivedAccount]]("listreceivedbyaccount",
List(JsNumber(confirmations),
JsBoolean(includeEmpty),
JsBoolean(includeWatchOnly)))
}
}

View File

@ -0,0 +1,49 @@
package org.bitcoins.rpc.client.v16
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.rpc.client.common.Client
import org.bitcoins.rpc.serializers.JsonReaders._
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsNumber, JsString}
import scala.concurrent.Future
/**
* RPC calls related to transaction sending
* specific to Bitcoin Core 0.16.
*/
trait V16SendRpc { self: Client =>
def move(
fromAccount: String,
toAccount: String,
amount: Bitcoins,
comment: String = ""): Future[Boolean] = {
bitcoindCall[Boolean]("move",
List(JsString(fromAccount),
JsString(toAccount),
JsNumber(amount.toBigDecimal),
JsNumber(6),
JsString(comment)))
}
def sendFrom(
fromAccount: String,
toAddress: BitcoinAddress,
amount: Bitcoins,
confirmations: Int = 1,
comment: String = "",
toComment: String = ""): Future[DoubleSha256DigestBE] = {
bitcoindCall[DoubleSha256DigestBE](
"sendfrom",
List(JsString(fromAccount),
JsString(toAddress.value),
JsNumber(amount.toBigDecimal),
JsNumber(confirmations),
JsString(comment),
JsString(toComment))
)
}
}

View File

@ -0,0 +1,103 @@
package org.bitcoins.rpc.client.v17
import akka.actor.ActorSystem
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionInput}
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.rpc.client.common.{
BitcoindRpcClient,
BitcoindVersion,
RpcOpts
}
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.rpc.jsonmodels.{
AddressInfoResult,
SignRawTransactionResult,
TestMempoolAcceptResult
}
import org.bitcoins.rpc.serializers.JsonSerializers._
import org.bitcoins.rpc.serializers.JsonWriters._
import play.api.libs.json.{JsArray, JsBoolean, JsString, Json}
import scala.concurrent.Future
import scala.util.Try
/**
* This class is compatible with version 0.17 of Bitcoin Core.
*
* @define signRawTx Bitcoin Core 0.17 had a breaking change in the API
* for signing raw transactions. Previously the same
* RPC call was used for signing a TX with existing keys
* in the Bitcoin Core wallet or a manually provided private key.
* These RPC calls are now separated out into two distinct calls.
*/
class BitcoindV17RpcClient(override val instance: BitcoindInstance)(
implicit
actorSystem: ActorSystem)
extends BitcoindRpcClient(instance)
with V17LabelRpc
with V17PsbtRpc {
override def version: BitcoindVersion = BitcoindVersion.V17
def getAddressInfo(address: BitcoinAddress): Future[AddressInfoResult] = {
bitcoindCall[AddressInfoResult]("getaddressinfo",
List(JsString(address.value)))
}
/**
* $signRawTx
*
* This RPC call signs the raw transaction with keys found in
* the Bitcoin Core wallet.
*/
def signRawTransactionWithWallet(
transaction: Transaction,
utxoDeps: Vector[RpcOpts.SignRawTransactionOutputParameter] = Vector.empty,
sigHash: HashType = HashType.sigHashAll
): Future[SignRawTransactionResult] =
bitcoindCall[SignRawTransactionResult]("signrawtransactionwithwallet",
List(JsString(transaction.hex),
Json.toJson(utxoDeps),
Json.toJson(sigHash)))
/**
* $signRawTx
*
* This RPC call signs the raw transaction with keys provided
* manually.
*/
def signRawTransactionWithKey(
transaction: Transaction,
keys: Vector[ECPrivateKey],
utxoDeps: Vector[RpcOpts.SignRawTransactionOutputParameter] = Vector.empty,
sigHash: HashType = HashType.sigHashAll
): Future[SignRawTransactionResult] =
bitcoindCall[SignRawTransactionResult]("signrawtransactionwithkey",
List(JsString(transaction.hex),
Json.toJson(keys),
Json.toJson(utxoDeps),
Json.toJson(sigHash)))
// testmempoolaccept expects (and returns) a list of txes,
// but currently only lists of length 1 is supported
def testMempoolAccept(
transaction: Transaction,
allowHighFees: Boolean = false): Future[TestMempoolAcceptResult] = {
bitcoindCall[Vector[TestMempoolAcceptResult]](
"testmempoolaccept",
List(JsArray(Vector(Json.toJson(transaction))), JsBoolean(allowHighFees)))
.map(_.head)
}
}
object BitcoindV17RpcClient {
def fromUnknownVersion(rpcClient: BitcoindRpcClient)(
implicit actorSystem: ActorSystem): Try[BitcoindV17RpcClient] =
Try {
new BitcoindV17RpcClient(rpcClient.instance)
}
}

View File

@ -0,0 +1,54 @@
package org.bitcoins.rpc.client.v17
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.rpc.client.common.Client
import org.bitcoins.rpc.client.common.RpcOpts.LabelPurpose
import org.bitcoins.rpc.jsonmodels.{LabelResult, ReceivedLabel}
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.{JsBoolean, JsNumber, JsString}
import scala.concurrent.Future
/**
* Bitcoin Core prior to version 0.17 had the concept of
* accounts. This has later been removed, and replaced
* with a label system, as well as functionality for
* having several distinct wallets active at the same time.
*/
trait V17LabelRpc { self: Client =>
def getAddressesByLabel(
label: String): Future[Map[BitcoinAddress, LabelResult]] = {
bitcoindCall[Map[BitcoinAddress, LabelResult]]("getaddressesbylabel",
List(JsString(label)))
}
def getReceivedByLabel(
account: String,
confirmations: Int = 1): Future[Bitcoins] = {
bitcoindCall[Bitcoins]("getreceivedbylabel",
List(JsString(account), JsNumber(confirmations)))
}
def setLabel(address: BitcoinAddress, label: String): Future[Unit] = {
bitcoindCall[Unit]("setlabel",
List(JsString(address.value), JsString(label)))
}
def listLabels(
purpose: Option[LabelPurpose] = None): Future[Vector[String]] = {
bitcoindCall[Vector[String]]("listlabels",
List(JsString(purpose.getOrElse("").toString)))
}
def listReceivedByLabel(
confirmations: Int = 1,
includeEmpty: Boolean = false,
includeWatchOnly: Boolean = false): Future[Vector[ReceivedLabel]] = {
bitcoindCall[Vector[ReceivedLabel]]("listreceivedbylabel",
List(JsNumber(confirmations),
JsBoolean(includeEmpty),
JsBoolean(includeWatchOnly)))
}
}

View File

@ -5,8 +5,10 @@ import java.net.URI
import java.nio.file.Paths
import com.typesafe.config._
import org.bitcoins.core.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}
/**
@ -15,7 +17,18 @@ import scala.util.{Failure, Properties, Success, Try}
sealed trait BitcoindInstance {
require(
rpcUri.getPort == rpcPort,
s"RpcUri and the rpcPort in authCredentials are different ${rpcUri} authcred: ${rpcPort}")
s"RpcUri and the rpcPort in authCredentials are different $rpcUri authcred: $rpcPort")
require(binary.exists,
s"bitcoind binary path (${binary.getAbsolutePath}) does not exist!")
// would like to check .canExecute as well, but we've run into issues on some machines
require(binary.isFile,
s"bitcoind binary path (${binary.getAbsolutePath}) must be a file")
/** The binary file that should get executed to start Bitcoin Core */
def binary: File
def network: NetworkParameters
def uri: URI
def rpcUri: URI
@ -23,6 +36,22 @@ sealed trait BitcoindInstance {
def zmqConfig: ZmqConfig
def rpcPort: Int = authCredentials.rpcPort
def getVersion: BitcoindVersion = {
val binaryPath = binary.getAbsolutePath
val foundVersion = Seq(binaryPath, "--version").!!.split("\n").head
.split(" ")
.last
foundVersion match {
case _: String if foundVersion.startsWith(BitcoindVersion.V16.toString) =>
BitcoindVersion.V16
case _: String if foundVersion.startsWith(BitcoindVersion.V17.toString) =>
BitcoindVersion.V17
case _: String => BitcoindVersion.Unknown
}
}
}
object BitcoindInstance {
@ -31,7 +60,8 @@ object BitcoindInstance {
uri: URI,
rpcUri: URI,
authCredentials: BitcoindAuthCredentials,
zmqConfig: ZmqConfig = ZmqConfig()
zmqConfig: ZmqConfig,
binary: File
) extends BitcoindInstance
def apply(
@ -39,17 +69,37 @@ object BitcoindInstance {
uri: URI,
rpcUri: URI,
authCredentials: BitcoindAuthCredentials,
zmqConfig: ZmqConfig = ZmqConfig()
zmqConfig: ZmqConfig = ZmqConfig(),
binary: File = DEFAULT_BITCOIND_LOCATION
): BitcoindInstance = {
BitcoindInstanceImpl(network,
uri,
rpcUri,
authCredentials,
zmqConfig = zmqConfig)
zmqConfig = zmqConfig,
binary = binary)
}
lazy val DEFAULT_BITCOIND_LOCATION: File = {
val path = Try("which bitcoind".!!)
.getOrElse(
throw new RuntimeException("Could not locate bitcoind on user PATH"))
new File(path.trim)
}
/**
* Taken from Bitcoin Wiki
* https://en.bitcoin.it/wiki/Data_directory
*/
private val DEFAULT_DATADIR =
Paths.get(Properties.userHome, ".bitcoin")
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")

View File

@ -69,7 +69,7 @@ object ZmqConfig {
Try(config.getString(path))
.map(str => Some(new URI(str)))
.getOrElse(throw new IllegalArgumentException(
s"$path in config is not a valid URI"))
s"$path (${config.getString(path)}) in config is not a valid URI"))
} else {
None
}

View File

@ -2,6 +2,7 @@ package org.bitcoins.rpc.jsonmodels
import org.bitcoins.core.crypto.{
DoubleSha256Digest,
DoubleSha256DigestBE,
ECPublicKey,
Sha256Hash160Digest
}
@ -72,7 +73,53 @@ case class MemoryManager(
chunks_free: Int)
extends OtherResult
case class ValidateAddressResult(
/**
* @note This is defined as a trait
* and not just a raw case class
* (as is done in other RPC return
* values) in order to make it possible
* to deprecate fields.
*/
trait ValidateAddressResult {
def isvalid: Boolean
def address: Option[BitcoinAddress]
def scriptPubKey: Option[ScriptPubKey]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def ismine: Option[Boolean]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def iswatchonly: Option[Boolean]
def isscript: Option[Boolean]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def script: Option[String]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def hex: Option[String]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def addresses: Option[Vector[BitcoinAddress]]
def sigsrequired: Option[Int]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def pubkey: Option[ECPublicKey]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def iscompressed: Option[Boolean]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def account: Option[String]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def hdkeypath: Option[String]
@deprecated("Use 'getaddressinfo' instead", since = "0.16")
def hdmasterkeyid: Option[Sha256Hash160Digest]
}
case class ValidateAddressResultImpl(
isvalid: Boolean,
address: Option[BitcoinAddress],
scriptPubKey: Option[ScriptPubKey],
@ -82,16 +129,22 @@ case class ValidateAddressResult(
script: Option[String],
hex: Option[String],
addresses: Option[Vector[BitcoinAddress]],
sigrequired: Option[Int],
sigsrequired: Option[Int],
pubkey: Option[ECPublicKey],
iscompressed: Option[Boolean],
account: Option[String],
hdkeypath: Option[String],
hdmasterkeyid: Option[Sha256Hash160Digest])
extends OtherResult
extends ValidateAddressResult
case class EstimateSmartFeeResult(
feerate: Option[BitcoinFeeUnit],
errors: Option[Vector[String]],
blocks: Int)
extends OtherResult
case class TestMempoolAcceptResult(
txid: DoubleSha256DigestBE,
allowed: Boolean,
rejectReason: Option[String]
)

View File

@ -27,17 +27,35 @@ case class RpcTransactionOutput(
scriptPubKey: RpcScriptPubKey)
extends RawTransactionResult
/**
* @see [[https://github.com/bitcoin/bitcoin/blob/fa6180188b8ab89af97860e6497716405a48bab6/src/script/standard.cpp#L27 standard.cpp]]
* from Bitcoin Core
*/
sealed abstract class RpcScriptType extends RawTransactionResult
object RpcScriptType {
final case object NONSTANDARD extends RpcScriptType
final case object PUBKEY extends RpcScriptType
final case object PUBKEYHASH extends RpcScriptType
final case object SCRIPTHASH extends RpcScriptType
final case object MULTISIG extends RpcScriptType
final case object NULLDATA extends RpcScriptType
final case object WITNESS_V0_KEYHASH extends RpcScriptType
final case object WITNESS_V0_SCRIPTHASH extends RpcScriptType
final case object WITNESS_UNKNOWN extends RpcScriptType
}
case class RpcScriptPubKey(
asm: String,
hex: String,
reqSigs: Option[Int],
scriptType: String,
scriptType: RpcScriptType,
addresses: Option[Vector[BitcoinAddress]])
extends RawTransactionResult
case class DecodeScriptResult(
asm: String,
typeOfScript: Option[String],
typeOfScript: Option[RpcScriptType],
reqSigs: Option[Int],
addresses: Option[Vector[P2PKHAddress]],
p2sh: P2SHAddress)
@ -60,10 +78,10 @@ case class GetRawTransactionResult(
locktime: UInt32,
vin: Vector[GetRawTransactionVin],
vout: Vector[RpcTransactionOutput],
blockhash: DoubleSha256DigestBE,
confirmations: Int,
time: UInt32,
blocktime: UInt32)
blockhash: Option[DoubleSha256DigestBE],
confirmations: Option[Int],
time: Option[UInt32],
blocktime: Option[UInt32])
extends RawTransactionResult
case class GetRawTransactionVin(

View File

@ -0,0 +1,70 @@
package org.bitcoins.rpc.jsonmodels
import org.bitcoins.core.crypto.{ECDigitalSignature, ECPublicKey}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.script.crypto.HashType
sealed abstract class RpcPsbtResult
final case class WalletProcessPsbtResult(psbt: String, complete: Boolean)
extends RpcPsbtResult
sealed abstract class FinalizePsbtResult extends RpcPsbtResult
final case class FinalizedPsbt(hex: Transaction) extends FinalizePsbtResult
final case class NonFinalizedPsbt(psbt: String) extends FinalizePsbtResult
final case class DecodePsbtResult(
tx: RpcTransaction,
unknown: Map[String, String],
inputs: Vector[RpcPsbtInput],
outputs: Vector[RpcPsbtOutput],
fee: Option[Bitcoins])
extends RpcPsbtResult
final case class RpcPsbtInput(
nonWitnessUtxo: Option[RpcTransaction],
witnessUtxo: Option[PsbtWitnessUtxoInput],
partialSignatures: Option[Map[ECPublicKey, ECDigitalSignature]],
sighash: Option[HashType],
redeemScript: Option[RpcPsbtScript],
witnessScript: Option[RpcPsbtScript],
bip32Derivs: Option[Vector[PsbtBIP32Deriv]],
finalScriptSig: Option[RpcPsbtScript],
finalScriptwitness: Option[Vector[String]], // todo(torkelrogstad) needs example of what this looks like
unknown: Option[Map[String, String]] // The unknown global fields
) extends RpcPsbtResult
final case class RpcPsbtScript(
asm: String, // todo(torkelrogstad) split into Vector[ScriptToken]?
hex: ScriptPubKey,
scriptType: Option[RpcScriptType],
address: Option[BitcoinAddress]
) extends RpcPsbtResult
final case class PsbtBIP32Deriv(
pubkey: ECPublicKey,
masterFingerprint: String, // todo(torkelrogstad)
path: String
// todo(torkelrogstad) there's more fields here
) extends RpcPsbtResult
final case class PsbtWitnessUtxoInput(
amount: Bitcoins,
scriptPubKey: RpcPsbtScript
) extends RpcPsbtResult
final case class RpcPsbtOutput(
redeemScript: Option[RpcPsbtScript],
witnessScript: Option[RpcPsbtScript],
bip32Derivs: Option[Vector[PsbtBIP32Deriv]],
unknown: Option[Map[String, String]]
) extends RpcPsbtResult
final case class WalletCreateFundedPsbtResult(
psbt: String, // todo change me
fee: Bitcoins,
changepos: Int
) extends RpcPsbtResult

View File

@ -2,13 +2,21 @@ package org.bitcoins.rpc.jsonmodels
import java.io.File
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, Sha256Hash160Digest}
import org.bitcoins.core.crypto.bip32.BIP32Path
import org.bitcoins.core.crypto.{
DoubleSha256DigestBE,
ECPublicKey,
RipeMd160Digest,
Sha256Hash160Digest
}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.script.{ScriptPubKey, WitnessVersion}
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.BitcoinFeeUnit
import org.bitcoins.rpc.client.common.RpcOpts.LabelPurpose
import org.joda.time.DateTime
sealed abstract class WalletResult
@ -108,6 +116,13 @@ case class ReceivedAccount(
lable: Option[String])
extends WalletResult
case class ReceivedLabel(
involvesWatchonly: Option[Boolean],
amount: Bitcoins,
confirmations: Int,
label: String)
extends WalletResult
case class ListSinceBlockResult(
transactions: Vector[Payment],
lastblock: DoubleSha256DigestBE)
@ -172,3 +187,39 @@ case class UnspentOutput(
spendable: Boolean,
solvable: Boolean)
extends WalletResult
case class AddressInfoResult(
address: BitcoinAddress,
scriptPubKey: ScriptPubKey,
ismine: Boolean,
iswatchonly: Boolean,
isscript: Boolean,
iswitness: Boolean,
iscompressed: Option[Boolean],
witness_version: Option[WitnessVersion],
witness_program: Option[String], // todo what's the correct type here?
script: Option[RpcScriptType],
hex: Option[ScriptPubKey],
pubkeys: Option[Vector[ECPublicKey]],
sigsrequired: Option[Int],
pubkey: Option[ECPublicKey],
embedded: Option[EmbeddedResult],
label: String,
timestamp: Option[DateTime],
hdkeypath: Option[BIP32Path],
hdseedid: Option[RipeMd160Digest],
hdmasterkeyid: Option[RipeMd160Digest],
labels: Vector[LabelResult])
extends WalletResult
case class EmbeddedResult(
isscript: Boolean,
iswitness: Boolean,
witness_version: WitnessVersion,
witness_program: Option[String],
pubkey: ECPublicKey,
address: BitcoinAddress,
scriptPubKey: ScriptPubKey)
extends WalletResult
case class LabelResult(name: String, purpose: LabelPurpose) extends WalletResult

View File

@ -3,12 +3,8 @@ package org.bitcoins.rpc.serializers
import java.io.File
import java.net.{InetAddress, URI}
import org.bitcoins.core.crypto.{
DoubleSha256Digest,
DoubleSha256DigestBE,
ECPublicKey,
Sha256Hash160Digest
}
import org.bitcoins.core.crypto.bip32.BIP32Path
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.number.{Int32, UInt32, UInt64}
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader, MerkleBlock}
@ -25,20 +21,27 @@ import org.bitcoins.core.protocol.{
P2SHAddress
}
import org.bitcoins.core.wallet.fee.BitcoinFeeUnit
import org.bitcoins.rpc.client.common.RpcOpts.AddressType
import org.bitcoins.rpc.jsonmodels._
import org.bitcoins.rpc.serializers.JsonReaders._
import org.bitcoins.rpc.serializers.JsonWriters._
import org.joda.time.DateTime
import play.api.libs.functional.syntax._
import play.api.libs.json._
object JsonSerializers {
implicit val bigIntReads: Reads[BigInt] = BigIntReads
implicit val dateTimeReads: Reads[DateTime] = DateTimeReads
// Internal Types
implicit val doubleSha256DigestReads: Reads[DoubleSha256Digest] =
DoubleSha256DigestReads
implicit val doubleSha256DigestBEReads: Reads[DoubleSha256DigestBE] =
DoubleSha256DigestBEReads
implicit val ripeMd160DigestReads: Reads[RipeMd160Digest] =
RipeMd160DigestReads
implicit val ripeMd160DigestBEReads: Reads[RipeMd160DigestBE] =
RipeMd160DigestBEReads
implicit val bitcoinsReads: Reads[Bitcoins] = BitcoinsReads
implicit val satoshisReads: Reads[Satoshis] = SatoshisReads
implicit val blockHeaderReads: Reads[BlockHeader] = BlockHeaderReads
@ -84,7 +87,7 @@ object JsonSerializers {
((__ \ "asm").read[String] and
(__ \ "hex").read[String] and
(__ \ "reqSigs").readNullable[Int] and
(__ \ "type").read[String] and
(__ \ "type").read[RpcScriptType] and
(__ \ "addresses").readNullable[Vector[BitcoinAddress]])(RpcScriptPubKey)
implicit val rpcTransactionOutputReads: Reads[RpcTransactionOutput] =
Json.reads[RpcTransactionOutput]
@ -93,7 +96,7 @@ object JsonSerializers {
implicit val decodeScriptResultReads: Reads[DecodeScriptResult] =
((__ \ "asm").read[String] and
(__ \ "type").readNullable[String] and
(__ \ "type").readNullable[RpcScriptType] and
(__ \ "reqSigs").readNullable[Int] and
(__ \ "addresses").readNullable[Vector[P2PKHAddress]] and
(__ \ "p2sh").read[P2SHAddress])(DecodeScriptResult)
@ -183,6 +186,15 @@ object JsonSerializers {
implicit val getTxOutSetInfoResultReads: Reads[GetTxOutSetInfoResult] =
Json.reads[GetTxOutSetInfoResult]
implicit val addressTypeWrites: Writes[AddressType] = AddressTypeWrites
implicit object Bip32PathFormats extends Format[BIP32Path] {
override def reads(json: JsValue): JsResult[BIP32Path] =
json.validate[String].map(BIP32Path.fromString)
override def writes(o: BIP32Path): JsValue =
JsString(o.toString)
}
// Wallet Models
implicit val multiSigReads: Reads[MultiSigResult] =
Json.reads[MultiSigResult]
@ -233,6 +245,9 @@ object JsonSerializers {
implicit val receivedAccountReads: Reads[ReceivedAccount] =
Json.reads[ReceivedAccount]
implicit val labelResult: Reads[LabelResult] =
Json.reads[LabelResult]
implicit val paymentReads: Reads[Payment] =
((__ \ "involvesWatchonly").readNullable[Boolean] and
(__ \ "account").readNullable[String] and
@ -297,12 +312,58 @@ object JsonSerializers {
implicit val getMemoryInfoResultReads: Reads[GetMemoryInfoResult] =
Json.reads[GetMemoryInfoResult]
implicit val validateAddressResultReads: Reads[ValidateAddressResult] =
Json.reads[ValidateAddressResult]
implicit val validateAddressResultReads: Reads[ValidateAddressResultImpl] =
Json.reads[ValidateAddressResultImpl]
implicit val embeddedResultReads: Reads[EmbeddedResult] =
Json.reads[EmbeddedResult]
implicit val addressInfoResultReads: Reads[AddressInfoResult] =
Json.reads[AddressInfoResult]
implicit val receivedLabelReads: Reads[ReceivedLabel] =
Json.reads[ReceivedLabel]
implicit val estimateSmartFeeResultReads: Reads[EstimateSmartFeeResult] =
Json.reads[EstimateSmartFeeResult]
implicit val walletProcessPsbtResultReads: Reads[WalletProcessPsbtResult] =
Json.reads[WalletProcessPsbtResult]
implicit val finalizedPsbtReads: Reads[FinalizedPsbt] = FinalizedPsbtReads
implicit val nonFinalizedPsbtReads: Reads[NonFinalizedPsbt] =
NonFinalizedPsbtReads
implicit val finalizePsbtResultReads: Reads[FinalizePsbtResult] =
FinalizePsbtResultReads
implicit val rpcPsbtOutputReads: Reads[RpcPsbtOutput] = RpcPsbtOutputReads
implicit val psbtBIP32DerivsReads: Reads[PsbtBIP32Deriv] =
PsbtBIP32DerivsReads
implicit val rpcPsbtScriptReads: Reads[RpcPsbtScript] = RpcPsbtScriptReads
implicit val psbtWitnessUtxoInputReads: Reads[PsbtWitnessUtxoInput] =
Json.reads[PsbtWitnessUtxoInput]
implicit val mapPubKeySignatureReads: Reads[
Map[ECPublicKey, ECDigitalSignature]] = MapPubKeySignatureReads
implicit val rpcPsbtInputReads: Reads[RpcPsbtInput] = RpcPsbtInputReads
implicit val decodePsbtResult: Reads[DecodePsbtResult] =
Json.reads[DecodePsbtResult]
implicit val walletCreateFundedPsbtResultReads: Reads[
WalletCreateFundedPsbtResult] = Json.reads[WalletCreateFundedPsbtResult]
implicit val rpcScriptTypeReads: Reads[RpcScriptType] = RpcScriptTypeReads
implicit val testMempoolAcceptResultReads: Reads[TestMempoolAcceptResult] =
TestMempoolAcceptResultReads
// Map stuff
implicit def mapDoubleSha256DigestReads: Reads[
Map[DoubleSha256Digest, GetMemPoolResult]] =
@ -314,6 +375,11 @@ object JsonSerializers {
Reads.mapReads[DoubleSha256DigestBE, GetMemPoolResult](s =>
JsSuccess(DoubleSha256DigestBE.fromHex(s)))
implicit def mapAddressesByLabelReads: Reads[
Map[BitcoinAddress, LabelResult]] =
Reads.mapReads[BitcoinAddress, LabelResult](s =>
JsSuccess(BitcoinAddress.fromString(s).get))
implicit val outputMapWrites: Writes[Map[BitcoinAddress, Bitcoins]] =
mapWrites[BitcoinAddress, Bitcoins](_.value)
}

View File

@ -7,10 +7,31 @@ import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionInput}
import org.bitcoins.core.script.crypto._
import org.bitcoins.core.util.BitcoinSUtil
import org.bitcoins.rpc.client.common.RpcOpts.{
AddressType,
WalletCreateFundedPsbtOptions
}
import play.api.libs.json._
import scala.collection.mutable
object JsonWriters {
implicit object HashTypeWrites extends Writes[HashType] {
override def writes(hash: HashType): JsValue = hash match {
case _: SIGHASH_ALL => JsString("ALL")
case _: SIGHASH_NONE => JsString("NONE")
case _: SIGHASH_SINGLE => JsString("SINGLE")
case _: SIGHASH_ALL_ANYONECANPAY => JsString("ALL|ANYONECANPAY")
case _: SIGHASH_NONE_ANYONECANPAY => JsString("NONE|ANYONECANPAY")
case _: SIGHASH_SINGLE_ANYONECANPAY => JsString("SINGLE|ANYONECANPAY")
case _: SIGHASH_ANYONECANPAY =>
throw new IllegalArgumentException(
"SIGHHASH_ANYONECANPAY is not supported by the bitcoind RPC interface")
}
}
implicit object BitcoinsWrites extends Writes[Bitcoins] {
override def writes(o: Bitcoins): JsValue = JsNumber(o.toBigDecimal)
}
@ -51,13 +72,42 @@ object JsonWriters {
implicit def mapWrites[K, V](keyString: K => String)(
implicit
vWrites: Writes[V]): Writes[Map[K, V]] = new Writes[Map[K, V]] {
override def writes(o: Map[K, V]): JsValue = {
Json.toJson(o.map { case (k, v) => (keyString(k), v) })
vWrites: Writes[V]): Writes[Map[K, V]] =
new Writes[Map[K, V]] {
override def writes(o: Map[K, V]): JsValue =
Json.toJson(o.map { case (k, v) => (keyString(k), v) })
}
}
implicit object MilliSatoshisWrites extends Writes[MilliSatoshis] {
override def writes(o: MilliSatoshis): JsValue = JsNumber(o.toBigDecimal)
}
implicit object AddressTypeWrites extends Writes[AddressType] {
override def writes(addr: AddressType): JsValue = JsString(addr.toString)
}
implicit object WalletCreateFundedPsbtOptionsWrites
extends Writes[WalletCreateFundedPsbtOptions] {
override def writes(opts: WalletCreateFundedPsbtOptions): JsValue = {
val jsOpts: mutable.Map[String, JsValue] = mutable.Map(
"includeWatching" -> JsBoolean(opts.includeWatching),
"lockUnspents" -> JsBoolean(opts.lockUnspents),
"replaceable" -> JsBoolean(opts.replaceable),
"estimate_mode" -> JsString(opts.estimateMode.toString)
)
def addToMapIfDefined[T](key: String, opt: Option[T])(
implicit writes: Writes[T]): Unit =
opt.foreach(o => jsOpts + (key -> Json.toJson(o)))
addToMapIfDefined("changeAddress", opts.changeAddress)
addToMapIfDefined("changePosition", opts.changePosition)
addToMapIfDefined("change_type", opts.changeType)
addToMapIfDefined("feeRate", opts.feeRate)
addToMapIfDefined("subtractFeeFromOutputs", opts.subtractFeeFromOutputs)
addToMapIfDefined("conf_target", opts.confTarget)
JsObject(jsOpts)
}
}
}

View File

@ -3,10 +3,12 @@ package org.bitcoins.rpc.util
import akka.actor.ActorSystem
import org.bitcoins.core.util.BitcoinSLogger
import scala.concurrent._
import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.concurrent.{Await, Future, Promise}
abstract class AsyncUtil extends BitcoinSLogger {
import AsyncUtil.DEFAULT_INTERNVAL
import AsyncUtil.DEFAULT_MAX_TRIES
private def retryRunnable(
condition: => Boolean,
@ -19,8 +21,9 @@ abstract class AsyncUtil extends BitcoinSLogger {
def retryUntilSatisfied(
condition: => Boolean,
duration: FiniteDuration,
maxTries: Int = 50)(implicit system: ActorSystem): Future[Unit] = {
duration: FiniteDuration = DEFAULT_INTERNVAL,
maxTries: Int = DEFAULT_MAX_TRIES)(
implicit system: ActorSystem): Future[Unit] = {
val f = () => Future.successful(condition)
retryUntilSatisfiedF(f, duration, maxTries)
}
@ -35,8 +38,9 @@ abstract class AsyncUtil extends BitcoinSLogger {
*/
def retryUntilSatisfiedF(
conditionF: () => Future[Boolean],
duration: FiniteDuration = 100.millis,
maxTries: Int = 50)(implicit system: ActorSystem): Future[Unit] = {
duration: FiniteDuration = DEFAULT_INTERNVAL,
maxTries: Int = DEFAULT_MAX_TRIES)(
implicit system: ActorSystem): Future[Unit] = {
val stackTrace: Array[StackTraceElement] =
Thread.currentThread().getStackTrace
@ -79,13 +83,16 @@ abstract class AsyncUtil extends BitcoinSLogger {
stackTrace: Array[StackTraceElement])(
implicit system: ActorSystem): Future[Unit] = {
implicit val ec = system.dispatcher
import system.dispatcher
conditionF().flatMap { condition =>
if (condition) {
Future.successful(())
} else if (counter == maxTries) {
Future.failed(RpcRetryException("Condition timed out", stackTrace))
Future.failed(
RpcRetryException(
s"Condition timed out after $maxTries attempts with $duration waiting periods",
stackTrace))
} else {
val p = Promise[Boolean]()
val runnable = retryRunnable(condition, p)
@ -106,22 +113,18 @@ abstract class AsyncUtil extends BitcoinSLogger {
}
/**
* Blocks until condition becomes true, the condition
* Returns a future that resolved when the condition becomes true, the condition
* is checked maxTries times, or overallTimeout is reached
* @param condition The blocking condition
* @param duration The interval between calls to check condition
* @param maxTries If condition is tried this many times, an exception is thrown
* @param overallTimeout If this much time passes, an exception is thrown.
* This exists in case calls to condition take significant time,
* otherwise just use duration and maxTries to configure timeout.
* @param system An ActorSystem to schedule calls to condition
*/
def awaitCondition(
condition: () => Boolean,
duration: FiniteDuration = 100.milliseconds,
maxTries: Int = 50,
overallTimeout: FiniteDuration = 1.hour)(
implicit system: ActorSystem): Unit = {
duration: FiniteDuration = DEFAULT_INTERNVAL,
maxTries: Int = DEFAULT_MAX_TRIES)(
implicit system: ActorSystem): Future[Unit] = {
//type hackery here to go from () => Boolean to () => Future[Boolean]
//to make sure we re-evaluate every time retryUntilSatisfied is called
@ -129,22 +132,32 @@ abstract class AsyncUtil extends BitcoinSLogger {
val conditionF: () => Future[Boolean] = () =>
Future.successful(conditionDef)
awaitConditionF(conditionF, duration, maxTries, overallTimeout)
awaitConditionF(conditionF, duration, maxTries)
}
def awaitConditionF(
conditionF: () => Future[Boolean],
duration: FiniteDuration = 100.milliseconds,
maxTries: Int = 50,
overallTimeout: FiniteDuration = 1.hour)(
implicit system: ActorSystem): Unit = {
duration: FiniteDuration = DEFAULT_INTERNVAL,
maxTries: Int = DEFAULT_MAX_TRIES)(
implicit system: ActorSystem): Future[Unit] = {
val f: Future[Unit] = retryUntilSatisfiedF(conditionF = conditionF,
duration = duration,
maxTries = maxTries)
retryUntilSatisfiedF(conditionF = conditionF,
duration = duration,
maxTries = maxTries)
Await.result(f, overallTimeout)
}
}
object AsyncUtil extends AsyncUtil
object AsyncUtil extends AsyncUtil {
/**
* The default interval between async attempts
*/
private[bitcoins] val DEFAULT_INTERNVAL: FiniteDuration = 100.milliseconds
/**
* The default number of async attempts before timing out
*/
private[bitcoins] val DEFAULT_MAX_TRIES: Int = 50
}

View File

@ -1,26 +1,43 @@
package org.bitcoins.rpc.util
import akka.actor.ActorSystem
import org.bitcoins.rpc.client.BitcoindRpcClient
import java.net.ServerSocket
import scala.concurrent.duration.{DurationInt, FiniteDuration}
import akka.actor.ActorSystem
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import scala.annotation.tailrec
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.duration.DurationInt
import scala.util.{Failure, Random, Success, Try}
abstract class RpcUtil extends AsyncUtil {
def awaitServer(
server: BitcoindRpcClient,
duration: FiniteDuration = 1.seconds,
maxTries: Int = 50)(implicit system: ActorSystem): Unit = {
val f = () => server.isStarted
awaitCondition(f, duration, maxTries)
}
def awaitServerShutdown(
server: BitcoindRpcClient,
duration: FiniteDuration = 300.milliseconds,
maxTries: Int = 50)(implicit system: ActorSystem): Unit = {
val f = () => !server.isStarted
awaitCondition(f, duration, maxTries)
maxTries: Int = 50)(implicit system: ActorSystem): Future[Unit] = {
retryUntilSatisfiedF(() => server.isStoppedF, duration, maxTries)
}
/**
* Generates a random port not in use
*/
@tailrec
final def randomPort: Int = {
val MAX = 65535 // max tcp port number
val MIN = 1025 // lowest port not requiring sudo
val port = Math.abs(Random.nextInt(MAX - MIN) + (MIN + 1))
val attempt = Try {
val socket = new ServerSocket(port)
socket.close()
socket.getLocalPort
}
attempt match {
case Success(value) => value
case Failure(_) => randomPort
}
}
}

View File

@ -2,6 +2,7 @@ import sbt.Credentials
import sbt.Keys.publishTo
import com.typesafe.sbt.SbtGit.GitKeys._
import scala.util.Properties
cancelable in Global := true
@ -85,9 +86,7 @@ lazy val commonSettings = List(
bintrayPublish
}
},
bintrayReleaseOnPublish := !isSnapshot.value,
//fix for https://github.com/sbt/sbt/issues/3519
updateOptions := updateOptions.value.withGigahorse(false),
git.formattedShaVersion := git.gitHeadCommit.value.map { sha =>
@ -105,7 +104,11 @@ lazy val commonSettings = List(
val file = (Test / sourceManaged).value / "amm.scala"
IO.write(file, """object amm extends App { ammonite.Main.main(args) }""")
Seq(file)
}.taskValue
}.taskValue,
// Travis has performance issues on macOS
Test / parallelExecution := !(Properties.isMac && sys.props
.get("CI")
.isDefined)
)
lazy val root = project

View File

@ -1,5 +1,6 @@
package org.bitcoins.core.protocol.transaction
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.protocol.script.{EmptyScriptSignature, ScriptSignature}
@ -42,6 +43,21 @@ object TransactionInput extends Factory[TransactionInput] {
extends TransactionInput
def empty: TransactionInput = EmptyTransactionInput
/**
* Generates a transaction input from the provided txid and output index.
* A script signature can also be provided, this defaults to an empty signature.
*/
def fromTxidAndVout(
txid: DoubleSha256DigestBE,
vout: UInt32,
signature: ScriptSignature = ScriptSignature.empty): TransactionInput = {
val outpoint = TransactionOutPoint(txid, vout)
TransactionInput(outPoint = outpoint,
scriptSignature = signature,
sequenceNumber = TransactionConstants.sequence)
}
def fromBytes(bytes: ByteVector): TransactionInput =
RawTransactionInputParser.read(bytes)

View File

@ -18,10 +18,9 @@ import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.eclair.rpc.client.EclairRpcClient
import org.bitcoins.eclair.rpc.config.{EclairAuthCredentials, EclairInstance}
import org.bitcoins.eclair.rpc.json._
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.testkit.eclair.rpc.{EclairNodes4, EclairRpcTestUtil}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.scalatest.{Assertion, AsyncFlatSpec, BeforeAndAfterAll}
import org.slf4j.Logger
@ -38,7 +37,7 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
val logger: Logger = BitcoinSLogger.logger
val bitcoindRpcClientF: Future[BitcoindRpcClient] = {
val cliF = BitcoindRpcTestUtil.startedBitcoindRpcClient()
val cliF = EclairRpcTestUtil.startedBitcoindRpcClient()
// make sure we have enough money open channels
//not async safe
val blocksF = cliF.flatMap(_.generate(200))
@ -113,7 +112,7 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll {
it should "be able to open and close a channel" in {
val changeAddrF = bitcoindRpcClientF.flatMap(_.getNewAddress())
val changeAddrF = bitcoindRpcClientF.flatMap(_.getNewAddress)
val result: Future[Assertion] = {
val isOpenedF: Future[(ChannelId, Assertion)] = {
val getChannelId =

View File

@ -15,7 +15,7 @@ class EclairRpcTestUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
ActorSystem.create("EclairRpcTestUtilTest")
private val bitcoindRpcF = {
val cliF = BitcoindRpcTestUtil.startedBitcoindRpcClient()
val cliF = EclairRpcTestUtil.startedBitcoindRpcClient()
val blocksF = cliF.flatMap(_.generate(200))
blocksF.flatMap(_ => cliF)
}
@ -49,13 +49,13 @@ class EclairRpcTestUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
for {
nodeInfoFirst <- first.getInfo
channelsFirst <- first.channels
channelsFirst <- first.channels()
nodeInfoSecond <- second.getInfo
channelsSecond <- second.channels
channelsSecond <- second.channels()
nodeInfoThird <- third.getInfo
channelsThird <- third.channels
channelsThird <- third.channels()
nodeInfoFourth <- fourth.getInfo
channelsFourth <- fourth.channels
channelsFourth <- fourth.channels()
} yield {
assert(channelsFirst.length == 1)
assert(channelsFirst.exists(_.nodeId == nodeInfoSecond.nodeId))

View File

@ -18,6 +18,7 @@ object Deps {
val nativeLoaderV = "2.3.2"
val typesafeConfigV = "1.3.3"
val ammoniteV = "1.6.2"
val asyncV = "0.9.7"
}
object Compile {
@ -37,6 +38,7 @@ object Deps {
}
object Test {
val async = "org.scala-lang.modules" %% "scala-async" % V.asyncV % "test" withSources () withJavadoc ()
val bitcoinj = ("org.bitcoinj" % "bitcoinj-core" % "0.14.4" % "test")
.exclude("org.slf4j", "slf4j-api")
@ -102,6 +104,7 @@ object Deps {
Test.logback,
Test.scalaTest,
Test.scalacheck,
Test.async,
Test.ammonite
)

View File

@ -1,4 +1,5 @@
package org.bitcoins.testkit.async
import akka.actor.ActorSystem
import org.scalatest.exceptions.{StackDepthException, TestFailedException}

View File

@ -18,8 +18,10 @@ import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.eclair.rpc.client.EclairRpcClient
import org.bitcoins.eclair.rpc.config.EclairInstance
import org.bitcoins.eclair.rpc.json.PaymentResult
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, BitcoindVersion}
import org.bitcoins.rpc.client.v16.BitcoindV16RpcClient
import org.bitcoins.rpc.config.{BitcoindInstance, ZmqConfig}
import org.bitcoins.rpc.util.RpcUtil
import org.bitcoins.testkit.async.TestAsyncUtil
import org.bitcoins.testkit.rpc.{BitcoindRpcTestUtil, TestRpcUtil}
@ -50,10 +52,44 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
lazy val network = RegTest
/**
* Makes a best effort to get a 0.16 bitcoind instance
*/
def startedBitcoindRpcClient(instance: BitcoindInstance = bitcoindInstance())(
implicit actorSystem: ActorSystem): Future[BitcoindRpcClient] = {
import actorSystem.dispatcher
for {
cli <- BitcoindRpcTestUtil.startedBitcoindRpcClient(instance)
// make sure we have enough money open channels
//not async safe
versionedCli: BitcoindRpcClient <- {
if (cli.instance.getVersion == BitcoindVersion.V17) {
val v16Cli = new BitcoindV16RpcClient(
BitcoindRpcTestUtil.v16Instance())
val startF =
Future.sequence(List(cli.stop(), v16Cli.start())).map(_ => v16Cli)
startF.recover {
case exception: Exception =>
logger.error(
List(
"Eclair requires Bitcoin Core 0.16.",
"You can set the environment variable BITCOIND_V16_PATH to override",
"the default bitcoind executable on your PATH."
).mkString(" "))
throw exception
}
} else {
Future.successful(cli)
}
}
} yield versionedCli
}
def bitcoindInstance(
port: Int = randomPort,
rpcPort: Int = randomPort,
zmqPort: Int = randomPort): BitcoindInstance = {
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)
@ -68,8 +104,8 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
//cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153
private def commonConfig(
bitcoindInstance: BitcoindInstance,
port: Int = randomPort,
apiPort: Int = randomPort): Config = {
port: Int = RpcUtil.randomPort,
apiPort: Int = RpcUtil.randomPort): Config = {
val configMap = {
Map(
"eclair.chain" -> "regtest",
@ -173,13 +209,6 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
new EclairRpcClient(inst)
}
def randomPort: Int = {
val firstAttempt = Math.abs(scala.util.Random.nextInt % 15000)
if (firstAttempt < network.port) {
firstAttempt + network.port
} else firstAttempt
}
def deleteTmpDir(dir: File): Boolean = {
if (!dir.isDirectory) {
dir.delete()
@ -378,8 +407,6 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
e.start().map(_ => e)
}
logger.debug(s"Both clients started")
val connectedLnF: Future[(EclairRpcClient, EclairRpcClient)] =
clientF.flatMap { c1 =>
otherClientF.flatMap { c2 =>
@ -536,7 +563,7 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
authCredentials =
eclairRpcClient.instance.authCredentials.bitcoinAuthOpt.get
)
new BitcoindRpcClient(bitcoindInstance)
new BitcoindRpcClient(bitcoindInstance)(system)
}
bitcoindRpc
}

View File

@ -46,7 +46,7 @@ abstract class BitcoinSUnitTest
}
object BitcoinSUnitTest {
private object BitcoinSUnitTest {
/** The number of times new code
* should be executed in a property based test

View File

@ -0,0 +1,36 @@
package org.bitcoins.testkit.util
import akka.actor.ActorSystem
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll}
import org.slf4j.{Logger, LoggerFactory}
import scala.collection.mutable
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext}
abstract class BitcoindRpcTest extends AsyncFlatSpec with BeforeAndAfterAll {
protected val logger: Logger = LoggerFactory.getLogger(getClass)
implicit val system: ActorSystem =
ActorSystem(getClass.getSimpleName, BitcoindRpcTestUtil.AKKA_CONFIG)
implicit val ec: ExecutionContext = system.dispatcher
implicit val networkParam: NetworkParameters = BitcoindRpcTestUtil.network
/**
* Bitcoind RPC clients can be added to this builder
* as they are created in tests. After tests have
* stopped running (either by succeeding or failing)
* all clients found in the builder is shut down.
*/
lazy val clientAccum: mutable.Builder[
BitcoindRpcClient,
Vector[BitcoindRpcClient]] = Vector.newBuilder
override protected def afterAll(): Unit = {
BitcoindRpcTestUtil.stopServers(clientAccum.result)
val _ = Await.result(system.terminate, 10.seconds)
}
}

View File

@ -0,0 +1,31 @@
package org.bitcoins.util
object ListUtil {
/**
* Generates all unique pairs of elements from `xs`
*/
def uniquePairs[T](xs: Vector[T]): Vector[(T, T)] =
for {
(x, idxX) <- xs.zipWithIndex
(y, idxY) <- xs.zipWithIndex if idxX < idxY
} yield (x, y)
/**
* Generates a vector of vectors "rotating" the head element
* over `xs`.
*
* {{{
* > slideFirst(Vector(1, 2, 3))
* Vector(Vector(1, 2, 3), Vector(2, 3, 1), Vector(3, 1, 2))
* }}}
*/
def rotateHead[T](xs: Vector[T]): Vector[Vector[T]] = {
for {
(x, idxX) <- xs.zipWithIndex
} yield {
val (firstHalf, secondHalf) = xs.splitAt(idxX)
secondHalf ++ firstHalf
}
}
}