mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
Add support for Bitcoin Core 0.17 (#384)
This commit is contained in:
parent
4d88354073
commit
fb16d6b200
@ -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
|
||||
|
@ -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))
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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))))
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)))
|
||||
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
package org.bitcoins.testkit.async
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.scalatest.exceptions.{StackDepthException, TestFailedException}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
31
testkit/src/main/scala/org/bitcoins/util/ListUtil.scala
Normal file
31
testkit/src/main/scala/org/bitcoins/util/ListUtil.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user