mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-22 14:22:39 +01:00
electrum: make docker tests run on windows/mac
Our electrumx docker container needs to contains to bitcoind that is running on the host. On linux we use the host network mode, which is not available on windows/osx On windows/osx we use host.docker.internal, which is not available on linux. This requires docker 18.03 or higher. electrum: change scripthash balance logging level to debug electrum: add a specific test with identical outputs electrum: rename wallet test electrum wallet: fix balance computation issue when different keys produced the exact same confirmed + unconfirmed balances, we would compute an invalid balance because these duplicates would be pruned. electrum: clean up tests, and add watcher docker tests add basic electrum wallet test our wallet connects to a dockerized electrumx server
This commit is contained in:
parent
2d829d40eb
commit
eecddcb340
9 changed files with 350 additions and 433 deletions
|
@ -1,4 +1,6 @@
|
|||
sudo: required
|
||||
services:
|
||||
-docker
|
||||
dist: trusty
|
||||
language: scala
|
||||
scala:
|
||||
|
|
|
@ -205,6 +205,18 @@
|
|||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<!-- TESTS -->
|
||||
<dependency>
|
||||
<groupId>com.whisk</groupId>
|
||||
<artifactId>docker-testkit-scalatest_${scala.version.short}</artifactId>
|
||||
<version>0.9.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.whisk</groupId>
|
||||
<artifactId>docker-testkit-impl-spotify_${scala.version.short}</artifactId>
|
||||
<version>0.9.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-testkit_${scala.version.short}</artifactId>
|
||||
|
|
|
@ -16,12 +16,8 @@
|
|||
|
||||
package fr.acinq.eclair.blockchain.bitcoind
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.Status.Failure
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
@ -37,35 +33,20 @@ import org.junit.runner.RunWith
|
|||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process.{Process, _}
|
||||
import scala.util.{Random, Try}
|
||||
import collection.JavaConversions._
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/bitcoinj-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BasicBitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val walletPassword = Random.alphanumeric.take(8).mkString
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BitcoinCoreWalletSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
startBitcoind()
|
||||
}
|
||||
|
||||
|
@ -178,40 +159,5 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLi
|
|||
|
||||
wallet.getBalance.pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Satoshi] > Satoshi(0))
|
||||
|
||||
}
|
||||
|
||||
private def startBitcoind(): Unit = {
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private def stopBitcoind(): Int = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
bitcoind.exitValue()
|
||||
}
|
||||
|
||||
private def waitForBitcoindReady(): Unit = {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](30 seconds)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2018 ACINQ SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fr.acinq.eclair.blockchain.bitcoind
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.{Files, StandardCopyOption}
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKitBase, TestProbe}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient}
|
||||
import fr.acinq.eclair.integration.IntegrationSpec
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.JValue
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
trait BitcoindService extends Logging {
|
||||
self: TestKitBase =>
|
||||
|
||||
implicit val system: ActorSystem
|
||||
|
||||
import scala.sys.process._
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
def startBitcoind(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) {
|
||||
Files.copy(classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath, StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
def stopBitcoind(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
bitcoind.exitValue()
|
||||
}
|
||||
|
||||
def waitForBitcoindReady(): Unit = {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](30 seconds)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 ACINQ SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.{JInt, JValue}
|
||||
import org.json4s.jackson.JsonMethods
|
||||
import org.junit.Ignore
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
@Ignore
|
||||
class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
require(System.getProperty("buildDirectory") != null, "please define system property buildDirectory")
|
||||
require(System.getProperty("electrumxPath") != null, "please define system property electrumxPath")
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
val PATH_ELECTRUMX_DBDIR = new File(INTEGRATION_TMP_DIR, "electrumx-db")
|
||||
val PATH_ELECTRUMX = new File(System.getProperty("electrumxPath"))
|
||||
|
||||
var bitcoind: Process = _
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = _
|
||||
var bitcoincli: ActorRef = _
|
||||
|
||||
var elecxtrumx: Process = _
|
||||
var electrumClient: ActorRef = _
|
||||
|
||||
case class BitcoinReq(method: String, params: Seq[Any] = Nil)
|
||||
|
||||
override protected def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method, Nil) =>
|
||||
bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) =>
|
||||
bitcoinrpcclient.invoke(method, params: _*) pipeTo sender
|
||||
}
|
||||
}))
|
||||
Files.createDirectories(PATH_ELECTRUMX_DBDIR.toPath)
|
||||
startBitcoind
|
||||
logger.info(s"generating initial blocks...")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500 :: Nil))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
startElectrum
|
||||
// FIXME use docker
|
||||
electrumClient = system.actorOf(Props(new ElectrumClient(new InetSocketAddress("localhost", 51001))))
|
||||
sender.send(electrumClient, ElectrumClient.AddStatusListener(sender.ref))
|
||||
sender.expectMsg(3 seconds, ElectrumClient.ElectrumReady)
|
||||
}
|
||||
|
||||
override protected def afterAll(): Unit = {
|
||||
logger.info(s"stopping bitcoind")
|
||||
stopBitcoind
|
||||
bitcoind.destroy()
|
||||
logger.info(s"stopping electrumx")
|
||||
elecxtrumx.destroy()
|
||||
}
|
||||
|
||||
def startBitcoind: Unit = {
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"bitcoind is ready")
|
||||
}
|
||||
|
||||
def stopBitcoind: Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
bitcoind.exitValue()
|
||||
}
|
||||
|
||||
def restartBitcoind: Unit = {
|
||||
stopBitcoind
|
||||
startBitcoind
|
||||
}
|
||||
|
||||
def startElectrum: Unit = {
|
||||
elecxtrumx = Process(s"$PATH_ELECTRUMX/electrumx_server.py",
|
||||
None,
|
||||
"DB_DIRECTORY" -> PATH_ELECTRUMX_DBDIR.getAbsolutePath,
|
||||
"DAEMON_URL" -> "foo:bar@localhost:28332",
|
||||
"COIN" -> "BitcoinSegwit",
|
||||
"NET" -> "regtest",
|
||||
"TCP_PORT" -> "51001").run()
|
||||
|
||||
logger.info(s"waiting for electrumx to initialize...")
|
||||
awaitCond({
|
||||
val result = s"$PATH_ELECTRUMX/electrumx_rpc.py getinfo".!!
|
||||
Try(JsonMethods.parse(result) \ "daemon_height") match {
|
||||
case Success(JInt(value)) if value.intValue() == 500 => true
|
||||
case _ => false
|
||||
}
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
}
|
||||
}
|
|
@ -16,16 +16,32 @@
|
|||
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import akka.actor.{ActorRef, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{AddStatusListener, BroadcastTransaction, BroadcastTransactionResponse}
|
||||
import org.json4s.JsonAST._
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.whisk.docker.DockerReadyChecker
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Btc, DeterministicWallet, MnemonicCode, Satoshi, Transaction, TxOut}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.{JDouble, JString, JValue}
|
||||
import org.junit.experimental.categories.Category
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
class ElectrumWalletSpec extends IntegrationSpec {
|
||||
trait DockerTest {}
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
@Category(Array(classOf[DockerTest]))
|
||||
class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging {
|
||||
|
||||
import ElectrumWallet._
|
||||
|
||||
|
@ -35,8 +51,45 @@ class ElectrumWalletSpec extends IntegrationSpec {
|
|||
logger.info(s"mnemonic codes for our wallet: $mnemonics")
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
var wallet: ActorRef = _
|
||||
var electrumClient: ActorRef = _
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
logger.info("starting bitcoind")
|
||||
startBitcoind()
|
||||
super.beforeAll()
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
logger.info("stopping bitcoind")
|
||||
stopBitcoind()
|
||||
super.afterAll()
|
||||
}
|
||||
|
||||
def getCurrentAddress(probe: TestProbe) = {
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
}
|
||||
|
||||
def getBalance(probe: TestProbe) = {
|
||||
probe.send(wallet, GetBalance)
|
||||
probe.expectMsgType[GetBalanceResponse]
|
||||
}
|
||||
|
||||
test("generate 500 blocks") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](30 seconds)
|
||||
DockerReadyChecker.LogLineContains("INFO:BlockProcessor:height: 501").looped(attempts = 15, delay = 1 second)
|
||||
}
|
||||
|
||||
test("wait until wallet is ready") {
|
||||
electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(new InetSocketAddress("localhost", 50001)))))
|
||||
wallet = system.actorOf(Props(new ElectrumWallet(seed, electrumClient, WalletParameters(Block.RegtestGenesisBlock.hash, minimumFee = Satoshi(5000)))), "wallet")
|
||||
val probe = TestProbe()
|
||||
awaitCond({
|
||||
|
@ -47,78 +100,114 @@ class ElectrumWalletSpec extends IntegrationSpec {
|
|||
logger.info(s"wallet is ready")
|
||||
}
|
||||
|
||||
ignore("receive funds") {
|
||||
test("receive funds") {
|
||||
val probe = TestProbe()
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe)
|
||||
logger.info(s"initial balance: $confirmed $unconfirmed")
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
// send money to our wallet
|
||||
val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe)
|
||||
|
||||
logger.info(s"sending 1 btc to $address")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
unconfirmed == Satoshi(100000000L)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
unconfirmed1 == unconfirmed + Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
// confirm our tx
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
confirmed == Satoshi(100000000L)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
confirmed1 == confirmed + Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address1) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
val GetCurrentReceiveAddressResponse(address1) = getCurrentAddress(probe)
|
||||
|
||||
logger.info(s"sending 1 btc to $address1")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1 :: 1.0 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1, 1.0))
|
||||
probe.expectMsgType[JValue]
|
||||
logger.info(s"sending 0.5 btc to $address1")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1 :: 0.5 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1, 0.5))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
confirmed == Satoshi(250000000L)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
confirmed1 == confirmed + Satoshi(250000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("handle transactions with identical outputs to us") {
|
||||
val probe = TestProbe()
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe)
|
||||
logger.info(s"initial balance: $confirmed $unconfirmed")
|
||||
|
||||
// send money to our wallet
|
||||
val amount = Satoshi(750000)
|
||||
val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe)
|
||||
val tx = Transaction(version = 2,
|
||||
txIn = Nil,
|
||||
txOut = Seq(
|
||||
TxOut(amount, fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)),
|
||||
TxOut(amount, fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash))
|
||||
), lockTime = 0L)
|
||||
val btcWallet = new BitcoinCoreWallet(bitcoinrpcclient)
|
||||
val future = for {
|
||||
FundTransactionResponse(tx1, pos, fee) <- btcWallet.fundTransaction(tx, false)
|
||||
SignTransactionResponse(tx2, true) <- btcWallet.signTransaction(tx1)
|
||||
txid <- btcWallet.publishTransaction(tx2)
|
||||
} yield txid
|
||||
val txid = Await.result(future, 10 seconds)
|
||||
|
||||
awaitCond({
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
unconfirmed1 == unconfirmed + amount + amount
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
confirmed1 == confirmed + amount + amount
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("receive 'confidence changed' notification") {
|
||||
val probe = TestProbe()
|
||||
val listener = TestProbe()
|
||||
system.eventStream.subscribe(listener.ref, classOf[WalletEvent])
|
||||
|
||||
listener.send(wallet, AddStatusListener(listener.ref))
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe)
|
||||
logger.info(s"initial balance $confirmed $unconfirmed")
|
||||
|
||||
logger.info(s"sending 1 btc to $address")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
val JString(txid) = probe.expectMsgType[JValue]
|
||||
logger.info(s"$txid send 1 btc to us at $address")
|
||||
logger.info(s"$txid sent 1 btc to us at $address")
|
||||
awaitCond({
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
unconfirmed1 - unconfirmed == Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
val TransactionReceived(tx, 0, received, sent, _) = listener.receiveOne(5 seconds)
|
||||
assert(tx.txid === BinaryData(txid))
|
||||
assert(received === Satoshi(100000000))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
logger.info("generating a new block")
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
confirmed1 - confirmed == Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
|
@ -130,15 +219,12 @@ class ElectrumWalletSpec extends IntegrationSpec {
|
|||
|
||||
test("send money to someone else (we broadcast)") {
|
||||
val probe = TestProbe()
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
val (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) = Base58Check.decode(address)
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe)
|
||||
|
||||
// create a tx that sends money to Bitcoin Core's address
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(1 btc, Script.pay2pkh(pubKeyHash)) :: Nil, lockTime = 0L)
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
|
||||
probe.send(wallet, CompleteTransaction(tx, 20000))
|
||||
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
|
||||
|
||||
|
@ -147,18 +233,17 @@ class ElectrumWalletSpec extends IntegrationSpec {
|
|||
probe.send(wallet, BroadcastTransaction(tx1))
|
||||
val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address))
|
||||
val JDouble(value) = probe.expectMsgType[JValue]
|
||||
value == 1.0
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 < confirmed - Btc(1) && confirmed1 > confirmed - Btc(1) - Satoshi(50000)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
@ -166,15 +251,13 @@ class ElectrumWalletSpec extends IntegrationSpec {
|
|||
|
||||
test("send money to ourselves (we broadcast)") {
|
||||
val probe = TestProbe()
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
val (Base58.Prefix.ScriptAddressTestnet, scriptHash) = Base58Check.decode(address)
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe)
|
||||
logger.info(s"current balance is $confirmed $unconfirmed")
|
||||
|
||||
// create a tx that sends money to Bitcoin Core's address
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(1 btc, OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil) :: Nil, lockTime = 0L)
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
|
||||
probe.send(wallet, CompleteTransaction(tx, 20000))
|
||||
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
|
||||
|
||||
|
@ -183,134 +266,13 @@ class ElectrumWalletSpec extends IntegrationSpec {
|
|||
probe.send(wallet, BroadcastTransaction(tx1))
|
||||
val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 < confirmed && confirmed1 > confirmed - Satoshi(50000)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
|
||||
logger.info(s"current balance is $confirmed $unconfirmed")
|
||||
confirmed1 < confirmed - Btc(1) && confirmed1 > confirmed - Btc(1) - Satoshi(50000)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
ignore("handle reorgs (pending receive)") {
|
||||
val probe = TestProbe()
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
|
||||
// send money to our receive address
|
||||
logger.info(s"sending 0.7 btc to $address")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 0.7 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
// generate 1 block
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
val JArray(List(JString(blockId))) = probe.expectMsgType[JValue]
|
||||
|
||||
// wait until our balance has been updated
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 == confirmed + Btc(0.7)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
// now invalidate the last block
|
||||
probe.send(bitcoincli, BitcoinReq("invalidateblock", blockId :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
// and restart bitcoind, which should remove pending wallet txs
|
||||
// bitcoind was started with -zapwallettxes=2
|
||||
stopBitcoind
|
||||
Thread.sleep(2000)
|
||||
startBitcoind
|
||||
Thread.sleep(2000)
|
||||
|
||||
|
||||
// generate 2 new blocks. the tx that sent us money is no longer there,
|
||||
// the corresponding utxo should have been removed and our balance should
|
||||
// be back to what it was before
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
val reorg = s"$PATH_ELECTRUMX/electrumx_rpc.py reorg 2".!!
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 == confirmed
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
ignore("handle reorgs (pending send)") {
|
||||
val probe = TestProbe()
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
val (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) = Base58Check.decode(address)
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"we start with a balance of $confirmed")
|
||||
|
||||
// create a tx that sends money to Bitcoin Core's address
|
||||
val amount = 0.5 btc
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, Script.pay2pkh(pubKeyHash)) :: Nil, lockTime = 0L)
|
||||
probe.send(wallet, CompleteTransaction(tx, 20000))
|
||||
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
|
||||
|
||||
// send it ourselves
|
||||
logger.info(s"sending $amount to $address with tx ${tx1.txid}")
|
||||
probe.send(wallet, BroadcastTransaction(tx1))
|
||||
val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
val JArray(List(JString(blockId))) = probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address :: Nil))
|
||||
val JDouble(value) = probe.expectMsgType[JValue]
|
||||
value == amount.amount.toDouble
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 < confirmed - amount && confirmed1 > confirmed - amount - Satoshi(50000)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
// now invalidate the last block
|
||||
probe.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val JInt(count) = probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("invalidateblock", blockId :: Nil))
|
||||
val foo = probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val JInt(count1) = probe.expectMsgType[JValue]
|
||||
|
||||
// and restart bitcoind, which should remove pending wallet txs
|
||||
// bitcoind was started with -zapwallettxes=2
|
||||
stopBitcoind
|
||||
Thread.sleep(2000)
|
||||
startBitcoind
|
||||
Thread.sleep(2000)
|
||||
|
||||
// generate 2 new blocks. the tx that sent us money is no longer there,
|
||||
// the corresponding utxo should have been removed and our balance should
|
||||
// be back to what it was before
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
val reorg = s"$PATH_ELECTRUMX/electrumx_rpc.py reorg 2".!!
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 == confirmed
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,70 +16,96 @@
|
|||
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import akka.actor.Props
|
||||
import akka.testkit.TestProbe
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.{ActorSystem, Props}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Base58, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.bitcoin.{Base58, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
|
||||
import fr.acinq.eclair.blockchain.{WatchConfirmed, WatchEventConfirmed, WatchEventSpent, WatchSpent}
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.{JArray, JString, JValue}
|
||||
import org.junit.experimental.categories.Category
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class ElectrumWatcherSpec extends IntegrationSpec {
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
@Category(Array(classOf[DockerTest]))
|
||||
class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging {
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
logger.info("starting bitcoind")
|
||||
startBitcoind()
|
||||
waitForBitcoindReady()
|
||||
super.beforeAll()
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
logger.info("stopping bitcoind")
|
||||
stopBitcoind()
|
||||
super.afterAll()
|
||||
}
|
||||
|
||||
test("watch for confirmed transactions") {
|
||||
val probe = TestProbe()
|
||||
val electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(new InetSocketAddress("localhost", 50001)))))
|
||||
val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient)))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
println(address)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
|
||||
val JString(hex) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction.read(hex)
|
||||
|
||||
val listener = TestProbe()
|
||||
probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut(0).publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 3 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 3))
|
||||
listener.expectNoMsg(1 second)
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds)
|
||||
system.stop(watcher)
|
||||
}
|
||||
|
||||
test("watch for spent transactions") {
|
||||
val probe = TestProbe()
|
||||
val electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(new InetSocketAddress("localhost", 50001)))))
|
||||
val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient)))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
println(address)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("dumpprivkey", address :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("dumpprivkey", address))
|
||||
val JString(wif) = probe.expectMsgType[JValue]
|
||||
val priv = PrivateKey.fromBase58(wif, Base58.Prefix.SecretKeyTestnet)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
val JString(txid) = probe.expectMsgType[JValue](30 seconds)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
|
||||
val JString(hex) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction.read(hex)
|
||||
|
||||
// find the output for the address we generated and create a tx that spends it
|
||||
val pos = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2pkh(priv.publicKey)))
|
||||
val pos = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey)))
|
||||
assert(pos != -1)
|
||||
val spendingTx = {
|
||||
val tmp = Transaction(version = 2,
|
||||
txIn = TxIn(OutPoint(tx, pos), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = TxOut(tx.txOut(pos).amount - Satoshi(1000), publicKeyScript = Script.pay2pkh(priv.publicKey)) :: Nil,
|
||||
txOut = TxOut(tx.txOut(pos).amount - Satoshi(1000), publicKeyScript = Script.pay2wpkh(priv.publicKey)) :: Nil,
|
||||
lockTime = 0)
|
||||
val sig = Transaction.signInput(tmp, 0, tx.txOut(pos).publicKeyScript, SIGHASH_ALL, tx.txOut(pos).amount, SigVersion.SIGVERSION_BASE, priv)
|
||||
val signedTx = tmp.updateSigScript(0, OP_PUSHDATA(sig) :: OP_PUSHDATA(priv.publicKey.toBin) :: Nil)
|
||||
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(priv.publicKey), SIGHASH_ALL, tx.txOut(pos).amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
|
||||
val signedTx = tmp.updateWitness(0, ScriptWitness(sig :: priv.publicKey.toBin :: Nil))
|
||||
Transaction.correctlySpends(signedTx, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
signedTx
|
||||
}
|
||||
|
@ -87,9 +113,9 @@ class ElectrumWatcherSpec extends IntegrationSpec {
|
|||
val listener = TestProbe()
|
||||
probe.send(watcher, WatchSpent(listener.ref, tx.txid, pos, tx.txOut(pos).publicKeyScript, BITCOIN_FUNDING_SPENT))
|
||||
listener.expectNoMsg(1 second)
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", spendingTx.toString :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", spendingTx.toString))
|
||||
probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
val blocks = probe.expectMsgType[JValue]
|
||||
val JArray(List(JString(block1), JString(block2))) = blocks
|
||||
val spent = listener.expectMsgType[WatchEventSpent](20 seconds)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2018 ACINQ SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import com.spotify.docker.client.{DefaultDockerClient, DockerClient}
|
||||
import com.whisk.docker.impl.spotify.SpotifyDockerFactory
|
||||
import com.whisk.docker.scalatest.DockerTestKit
|
||||
import com.whisk.docker.{DockerContainer, DockerFactory, LogLineReceiver}
|
||||
import org.scalatest.Suite
|
||||
|
||||
trait ElectrumxService extends DockerTestKit {
|
||||
self: Suite =>
|
||||
|
||||
val electrumxContainer = if (System.getProperty("os.name").startsWith("Linux")) {
|
||||
// "host" mode will let the container access the host network on linux
|
||||
DockerContainer("lukechilds/electrumx")
|
||||
.withNetworkMode("host")
|
||||
.withEnv("DAEMON_URL=http://foo:bar@localhost:28332", "COIN=BitcoinSegwit", "NET=regtest")
|
||||
.withLogLineReceiver(LogLineReceiver(true, println))
|
||||
} else {
|
||||
// on windows or oxs, host mode is not available, but from docker 18.03 on host.docker.internal can be used instead
|
||||
// host.docker.internal is not (yet ?) available on linux though
|
||||
DockerContainer("lukechilds/electrumx")
|
||||
.withPorts(50001 -> Some(50001))
|
||||
.withEnv("DAEMON_URL=http://foo:bar@host.docker.internal:28332", "COIN=BitcoinSegwit", "NET=regtest", "TCP_PORT=50001")
|
||||
.withLogLineReceiver(LogLineReceiver(true, println))
|
||||
}
|
||||
|
||||
override def dockerContainers: List[DockerContainer] = electrumxContainer :: super.dockerContainers
|
||||
|
||||
private val client: DockerClient = DefaultDockerClient.fromEnv().build()
|
||||
|
||||
override implicit val dockerFactory: DockerFactory = new SpotifyDockerFactory(client)
|
||||
}
|
|
@ -17,17 +17,16 @@
|
|||
package fr.acinq.eclair.integration
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.nio.file.Files
|
||||
import java.util.{Properties, UUID}
|
||||
import java.util.Properties
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, BinaryData, Block, Crypto, MilliSatoshi, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
import fr.acinq.eclair.channel._
|
||||
|
@ -51,58 +50,30 @@ import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
|||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
/**
|
||||
* Created by PM on 15/03/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var nodes: Map[String, Kit] = Map()
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
}
|
||||
}))
|
||||
startBitcoind()
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
bitcoind.exitValue()
|
||||
stopBitcoind()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
setup.system.terminate()
|
||||
}
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
|
|
Loading…
Add table
Reference in a new issue