Compare commits
21 Commits
applicatio
...
docker2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab2c91b5e0 | ||
|
|
5f6987bc1a | ||
|
|
82c5b676e6 | ||
|
|
71f39a033b | ||
|
|
5b2fbb1bb2 | ||
|
|
1f36e8261e | ||
|
|
552fcd3a73 | ||
|
|
1c7f66bf5b | ||
|
|
6e37586f9e | ||
|
|
b2c684a0da | ||
|
|
84a9d73413 | ||
|
|
44e7c3ba31 | ||
|
|
ffc4172e70 | ||
|
|
2ef479d38c | ||
|
|
2d5d68bf3f | ||
|
|
ba25821e85 | ||
|
|
86133db5f7 | ||
|
|
40b18aed8b | ||
|
|
8b151eb5c0 | ||
|
|
6304041d77 | ||
|
|
bf0423c020 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.git
|
||||
**/*.idea
|
||||
**/*.iml
|
||||
**/target
|
||||
10
BUILD.md
10
BUILD.md
@ -8,14 +8,18 @@
|
||||
## Build
|
||||
To build the project, simply run:
|
||||
```shell
|
||||
$ mvn package
|
||||
$ mvn install
|
||||
```
|
||||
To skip the tests, run:
|
||||
```shell
|
||||
$ mvn package -DskipTests
|
||||
$ mvn install -DskipTests
|
||||
```
|
||||
To only build the `eclair-node` module
|
||||
```shell
|
||||
$ mvn install -pl eclair-node -am -DskipTests
|
||||
```
|
||||
To generate the windows installer along with the build, run the following command:
|
||||
```shell
|
||||
$ mvn package -DskipTests -Pinstaller
|
||||
$ mvn install -DskipTests -Pinstaller
|
||||
```
|
||||
The generated installer will be located in `eclair-node-gui/target/jfx/installer`
|
||||
|
||||
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@ -0,0 +1,49 @@
|
||||
FROM openjdk:8u121-jdk-alpine as BUILD
|
||||
|
||||
# Setup maven, we don't use https://hub.docker.com/_/maven/ as it declare .m2 as volume, we loose all mvn cache
|
||||
# We can alternatively do as proposed by https://github.com/carlossg/docker-maven#packaging-a-local-repository-with-the-image
|
||||
# this was meant to make the image smaller, but we use multi-stage build so we don't care
|
||||
|
||||
RUN apk add --no-cache curl tar bash
|
||||
|
||||
ARG MAVEN_VERSION=3.5.2
|
||||
ARG USER_HOME_DIR="/root"
|
||||
ARG SHA=707b1f6e390a65bde4af4cdaf2a24d45fc19a6ded00fff02e91626e3e42ceaff
|
||||
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
|
||||
|
||||
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
|
||||
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
|
||||
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha256sum -c - \
|
||||
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
|
||||
&& rm -f /tmp/apache-maven.tar.gz \
|
||||
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
|
||||
|
||||
ENV MAVEN_HOME /usr/share/maven
|
||||
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
|
||||
|
||||
# Let's fetch eclair dependencies, so that Docker can cache them
|
||||
# This way we won't have to fetch dependencies again if only the source code changes
|
||||
# The easiest way to reliably get dependencies is to build the project with no sources
|
||||
WORKDIR /usr/src
|
||||
COPY pom.xml pom.xml
|
||||
COPY eclair-core/pom.xml eclair-core/pom.xml
|
||||
COPY eclair-node/pom.xml eclair-node/pom.xml
|
||||
COPY eclair-node-gui/pom.xml eclair-node-gui/pom.xml
|
||||
RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala
|
||||
# Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core
|
||||
RUN mvn install -pl eclair-node -am clean
|
||||
|
||||
# Only then do we copy the sources
|
||||
COPY . .
|
||||
|
||||
# And this time we can build in offline mode
|
||||
RUN mvn package -pl eclair-node -am -DskipTests -o
|
||||
# It might be good idea to run the tests here, so that the docker build fail if the code is bugged
|
||||
|
||||
# We currently use a debian image for runtime because of some jni-related issue with sqlite
|
||||
FROM openjdk:8u151-jre-slim
|
||||
WORKDIR /app
|
||||
# Eclair only needs the eclair-node-*.jar to run
|
||||
COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar .
|
||||
RUN ln `ls` eclair-node
|
||||
ENTRYPOINT [ "java", "-jar", "eclair-node" ]
|
||||
@ -44,6 +44,12 @@ zmqpubrawblock=tcp://127.0.0.1:29000
|
||||
zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
```
|
||||
|
||||
On **__testnet__**, you also need to make sure that all your UTXOs are `p2sh-of-p2wpkh`.
|
||||
To do this, use the debug console, create a new address with `getnewaddress`, import it as a witness address with `addwitnessaddress`, and
|
||||
send all your balance to this witness address.
|
||||
If you need to create and send funds manually, don't forget to create and specify a witness address for the change output (this option is avaliable on the GUI once you set the `Enable coin control features` wallet option).
|
||||
|
||||
|
||||
### Installing Eclair
|
||||
|
||||
The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases).
|
||||
@ -125,6 +131,7 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
|
||||
channel | channelId | retrieve detailed information about a given channel
|
||||
allnodes | | list all known nodes
|
||||
allchannels | | list all known channels
|
||||
receive | description | generate a payment request without a required amount (can be useful for donations)
|
||||
receive | amountMsat, description | generate a payment request for a given amount
|
||||
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
|
||||
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
|
||||
|
||||
@ -28,13 +28,16 @@ case $1 in
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
|
||||
;;
|
||||
"receive")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"something\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"${3?"missing description"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"send")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"network")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"network\", \"params\" : [] }' $URL" | jq ".result"
|
||||
"allnodes")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"allnodes\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"allchannels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"allchannels\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"peers")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
|
||||
|
||||
@ -15,17 +15,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.googlecode.maven-download-plugin</groupId>
|
||||
<artifactId>download-maven-plugin</artifactId>
|
||||
|
||||
@ -48,7 +48,7 @@ eclair {
|
||||
global-features = ""
|
||||
local-features = "08" // initial_routing_sync
|
||||
channel-flags = 1 // announce channels
|
||||
dust-limit-satoshis = 542
|
||||
dust-limit-satoshis = 546
|
||||
default-feerate-per-kb = 20000 // default bitcoin core value
|
||||
|
||||
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
|
||||
@ -81,21 +81,4 @@ eclair {
|
||||
auto-reconnect = true
|
||||
|
||||
payment-handler = "local"
|
||||
}
|
||||
akka {
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
loglevel = "DEBUG"
|
||||
|
||||
actor {
|
||||
debug {
|
||||
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
|
||||
fsm = on
|
||||
}
|
||||
}
|
||||
|
||||
http {
|
||||
host-connection-pool {
|
||||
max-open-requests = 64
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet}
|
||||
import fr.acinq.eclair.NodeParams.WatcherType
|
||||
import fr.acinq.eclair.channel.Channel
|
||||
import fr.acinq.eclair.db._
|
||||
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqliteNetworkDb, SqlitePeersDb, SqlitePreimagesDb}
|
||||
|
||||
@ -114,6 +115,14 @@ object NodeParams {
|
||||
case _ => BITCOIND
|
||||
}
|
||||
|
||||
val dustLimitSatoshis = config.getLong("dust-limit-satoshis")
|
||||
if (chainHash == Block.LivenetGenesisBlock.hash) {
|
||||
require(dustLimitSatoshis >= Channel.MIN_DUSTLIMIT, s"dust limit must be greater than ${Channel.MIN_DUSTLIMIT}")
|
||||
}
|
||||
|
||||
val maxAcceptedHtlcs = config.getInt("max-accepted-htlcs")
|
||||
require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}")
|
||||
|
||||
NodeParams(
|
||||
extendedPrivateKey = extendedPrivateKey,
|
||||
privateKey = extendedPrivateKey.privateKey,
|
||||
@ -122,9 +131,9 @@ object NodeParams {
|
||||
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
|
||||
globalFeatures = BinaryData(config.getString("global-features")),
|
||||
localFeatures = BinaryData(config.getString("local-features")),
|
||||
dustLimitSatoshis = config.getLong("dust-limit-satoshis"),
|
||||
dustLimitSatoshis = dustLimitSatoshis,
|
||||
maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")),
|
||||
maxAcceptedHtlcs = config.getInt("max-accepted-htlcs"),
|
||||
maxAcceptedHtlcs = maxAcceptedHtlcs,
|
||||
expiryDeltaBlocks = config.getInt("expiry-delta-blocks"),
|
||||
htlcMinimumMsat = config.getInt("htlc-minimum-msat"),
|
||||
delayBlocks = config.getInt("delay-blocks"),
|
||||
|
||||
@ -70,12 +70,17 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
progress = (json \ "verificationprogress").extract[Double]
|
||||
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse))
|
||||
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
|
||||
} yield (progress, chainHash, bitcoinVersion)
|
||||
unspentAddresses <- bitcoinClient.listUnspentAddresses
|
||||
} yield (progress, chainHash, bitcoinVersion, unspentAddresses)
|
||||
// blocking sanity checks
|
||||
val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds)
|
||||
val (progress, chainHash, bitcoinVersion, unspentAddresses) = Await.result(future, 10 seconds)
|
||||
assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)")
|
||||
if (chainHash == Block.TestnetGenesisBlock.hash) {
|
||||
assert(unspentAddresses.forall(isSegwitAddress), "In testnet mode, make sure that all your UTXOs are p2sh-of-p2wpkh (check out our README for more details)")
|
||||
}
|
||||
assert(progress > 0.99, "bitcoind should be synchronized")
|
||||
// TODO: add a check on bitcoin version?
|
||||
|
||||
Bitcoind(bitcoinClient)
|
||||
case BITCOINJ =>
|
||||
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
|
||||
@ -107,8 +112,8 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
logger.info(s"initial feeratesPerByte=${Globals.feeratesPerByte.get()}")
|
||||
val feeProvider = (chain, bitcoin) match {
|
||||
case ("regtest", _) => new ConstantFeeProvider(defaultFeerates)
|
||||
case (_, Bitcoind(client)) => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
case _ => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
case (_, Bitcoind(client)) => new FallbackFeeProvider(new BitgoFeeProvider() :: new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
case _ => new FallbackFeeProvider(new BitgoFeeProvider() :: new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
}
|
||||
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
|
||||
case feerates: FeeratesPerByte =>
|
||||
@ -131,7 +136,7 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
}
|
||||
|
||||
val wallet = bitcoin match {
|
||||
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
|
||||
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient)
|
||||
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
|
||||
case Electrum(electrumClient) =>
|
||||
val electrumSeedPath = new File(datadir, "electrum_seed.dat")
|
||||
|
||||
@ -95,8 +95,10 @@ trait Service extends Logging {
|
||||
(router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId))
|
||||
case JsonRPCBody(_, _, "allchannels", _) =>
|
||||
(router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2)))
|
||||
case JsonRPCBody(_, _, "receive", JString(description) :: Nil) =>
|
||||
(paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
case JsonRPCBody(_, _, "receive", JInt(amountMsat) :: JString(description) :: Nil) =>
|
||||
(paymentHandler ? ReceivePayment(MilliSatoshi(amountMsat.toLong), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
(paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
case JsonRPCBody(_, _, "send", JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil) =>
|
||||
(paymentInitiator ? SendPayment(amountMsat.toLong, paymentHash, PublicKey(nodeId))).mapTo[PaymentResult]
|
||||
case JsonRPCBody(_, _, "send", JString(paymentRequest) :: rest) =>
|
||||
|
||||
@ -23,7 +23,7 @@ final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, publicKeySc
|
||||
object WatchConfirmed {
|
||||
// if we have the entire transaction, we can get the redeemScript from the witness, and re-compute the publicKeyScript
|
||||
// we support both p2pkh and p2wpkh scripts
|
||||
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, extractPublicKeyScript(tx.txIn.head.witness), minDepth, event)
|
||||
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, tx.txOut.map(_.publicKeyScript).headOption.getOrElse(""), minDepth, event)
|
||||
|
||||
def extractPublicKeyScript(witness: ScriptWitness): BinaryData = Try(PublicKey(witness.stack.last)) match {
|
||||
case Success(pubKey) =>
|
||||
|
||||
@ -1,55 +1,32 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoind
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Base58Check, BinaryData, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
|
||||
import akka.actor.ActorSystem
|
||||
import fr.acinq.bitcoin.{BinaryData, OutPoint, Satoshi, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError}
|
||||
import fr.acinq.eclair.channel.{BITCOIN_OUTPUT_SPENT, BITCOIN_TX_CONFIRMED}
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.{JBool, JDouble, JInt, JString}
|
||||
import org.json4s.JsonAST._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Due to bitcoin-core wallet not fully supporting segwit txes yet, our current scheme is:
|
||||
* utxos <- parent-tx <- funding-tx
|
||||
*
|
||||
* With:
|
||||
* - utxos may be non-segwit
|
||||
* - parent-tx pays to a p2wpkh segwit output
|
||||
* - funding-tx is a segwit tx
|
||||
*
|
||||
* Created by PM on 06/07/2017.
|
||||
*/
|
||||
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
|
||||
override def getBalance: Future[Satoshi] = ???
|
||||
import BitcoinCoreWallet._
|
||||
|
||||
override def getFinalAddress: Future[String] = rpcClient.invoke("getnewaddress").map(json => {
|
||||
val JString(address) = json
|
||||
address
|
||||
})
|
||||
|
||||
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
|
||||
|
||||
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
|
||||
|
||||
case class MakeFundingTxResponseWithParent(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey)
|
||||
|
||||
def fundTransaction(hex: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
|
||||
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(lockUnspents)).map(json => {
|
||||
def fundTransaction(hex: String, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
|
||||
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(changeAddress, lockUnspents)).map(json => {
|
||||
val JString(hex) = json \ "hex"
|
||||
val JInt(changepos) = json \ "changepos"
|
||||
val JDouble(fee) = json \ "fee"
|
||||
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
|
||||
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), (fee * 10e8).toLong)
|
||||
})
|
||||
}
|
||||
|
||||
def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] =
|
||||
fundTransaction(Transaction.write(tx).toString(), lockUnspents)
|
||||
def fundTransaction(tx: Transaction, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toString(), changeAddress, lockUnspents)
|
||||
|
||||
def signTransaction(hex: String): Future[SignTransactionResponse] =
|
||||
rpcClient.invoke("signrawtransaction", hex).map(json => {
|
||||
@ -58,157 +35,66 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(impl
|
||||
SignTransactionResponse(Transaction.read(hex), complete)
|
||||
})
|
||||
|
||||
def signTransaction(tx: Transaction): Future[SignTransactionResponse] =
|
||||
signTransaction(Transaction.write(tx).toString())
|
||||
def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toString())
|
||||
|
||||
def getTransaction(txid: BinaryData): Future[Transaction] = {
|
||||
rpcClient.invoke("getrawtransaction", txid.toString()).map(json => {
|
||||
val JString(hex) = json
|
||||
Transaction.read(hex)
|
||||
})
|
||||
}
|
||||
def getTransaction(txid: BinaryData): Future[Transaction] = rpcClient.invoke("getrawtransaction", txid.toString()) collect { case JString(hex) => Transaction.read(hex) }
|
||||
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
|
||||
publishTransaction(Transaction.write(tx).toString())
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = publishTransaction(Transaction.write(tx).toString())
|
||||
|
||||
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
|
||||
rpcClient.invoke("sendrawtransaction", hex) collect {
|
||||
case JString(txid) => txid
|
||||
}
|
||||
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = rpcClient.invoke("sendrawtransaction", hex) collect { case JString(txid) => txid }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fundingTxResponse a funding tx response
|
||||
* @return an updated funding tx response that is properly sign
|
||||
*/
|
||||
def sign(fundingTxResponse: MakeFundingTxResponseWithParent): MakeFundingTxResponseWithParent = {
|
||||
// find the output that we are spending from
|
||||
val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
|
||||
def unlockOutpoint(outPoints: List[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("lockunspent", true, outPoints.map(outPoint => Utxo(outPoint.txid.toString, outPoint.index))) collect { case JBool(result) => result }
|
||||
|
||||
val pub = fundingTxResponse.priv.publicKey
|
||||
val pubKeyScript = Script.pay2pkh(pub)
|
||||
val sig = Transaction.signInput(fundingTxResponse.fundingTx, 0, pubKeyScript, SIGHASH_ALL, utxo.amount, SigVersion.SIGVERSION_WITNESS_V0, fundingTxResponse.priv)
|
||||
val witness = ScriptWitness(Seq(sig, pub.toBin))
|
||||
val fundingTx1 = fundingTxResponse.fundingTx.updateSigScript(0, OP_PUSHDATA(Script.write(Script.pay2wpkh(pub))) :: Nil).updateWitness(0, witness)
|
||||
|
||||
Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
fundingTxResponse.copy(fundingTx = fundingTx1)
|
||||
}
|
||||
override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDouble(balance) => Satoshi((balance * 10e8).toLong) }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fundingTxResponse funding transaction response, which includes a funding tx, its parent, and the private key
|
||||
* that we need to re-sign the funding
|
||||
* @param newParentTx new parent tx
|
||||
* @return an updated funding transaction response where the funding tx now spends from newParentTx
|
||||
*/
|
||||
def replaceParent(fundingTxResponse: MakeFundingTxResponseWithParent, newParentTx: Transaction): MakeFundingTxResponseWithParent = {
|
||||
// find the output that we are spending from
|
||||
val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
|
||||
override def getFinalAddress: Future[String] = for {
|
||||
JString(address) <- rpcClient.invoke("getnewaddress")
|
||||
// we want bitcoind to only use segwit addresses to avoid malleability issues
|
||||
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
|
||||
} yield segwitAddress
|
||||
|
||||
// check that it matches what we expect, which is a P2WPKH output to our public key
|
||||
require(utxo.publicKeyScript == Script.write(Script.pay2sh(Script.pay2wpkh(fundingTxResponse.priv.publicKey))))
|
||||
|
||||
// update our tx input we the hash of the new parent
|
||||
val input = fundingTxResponse.fundingTx.txIn(0)
|
||||
val input1 = input.copy(outPoint = input.outPoint.copy(hash = newParentTx.hash))
|
||||
val unsignedFundingTx = fundingTxResponse.fundingTx.copy(txIn = Seq(input1))
|
||||
|
||||
// and re-sign it
|
||||
sign(MakeFundingTxResponseWithParent(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv))
|
||||
}
|
||||
|
||||
def makeParentAndFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponseWithParent] =
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
|
||||
for {
|
||||
// ask for a new address and the corresponding private key
|
||||
JString(address) <- rpcClient.invoke("getnewaddress")
|
||||
JString(wif) <- rpcClient.invoke("dumpprivkey", address)
|
||||
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
|
||||
(prefix, raw) = Base58Check.decode(wif)
|
||||
priv = PrivateKey(raw, compressed = true)
|
||||
pub = priv.publicKey
|
||||
// create a tx that sends money to a P2SH(WPKH) output that matches our private key
|
||||
parentFee = Satoshi(250 * 2 * 2 * feeRatePerKw / 1024)
|
||||
partialParentTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Nil,
|
||||
txOut = TxOut(amount + parentFee, Script.pay2sh(Script.pay2wpkh(pub))) :: Nil,
|
||||
lockTime = 0L)
|
||||
FundTransactionResponse(unsignedParentTx, _, _) <- fundTransaction(partialParentTx, lockUnspents = true)
|
||||
// this is the first tx that we will publish, a standard tx which send money to our p2wpkh address
|
||||
SignTransactionResponse(parentTx, true) <- signTransaction(unsignedParentTx)
|
||||
// now we create the funding tx
|
||||
// we create a new segwit change address (we don't want bitcoin core to use regular malleable outputs)
|
||||
JString(changeAddress) <- rpcClient.invoke("getnewaddress")
|
||||
JString(segwitChangeAddress) <- rpcClient.invoke("addwitnessaddress", changeAddress)
|
||||
_ = logger.debug(s"using segwitChangeAddress=$segwitChangeAddress")
|
||||
// partial funding tx
|
||||
partialFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(amount, pubkeyScript) :: Nil,
|
||||
lockTime = 0)
|
||||
// and update it to spend from our segwit tx
|
||||
pos = Transactions.findPubKeyScriptIndex(parentTx, Script.pay2sh(Script.pay2wpkh(pub)))
|
||||
unsignedFundingTx = partialFundingTx.copy(txIn = TxIn(OutPoint(parentTx, pos), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil)
|
||||
} yield sign(MakeFundingTxResponseWithParent(parentTx, unsignedFundingTx, 0, priv))
|
||||
|
||||
/**
|
||||
* This is a workaround for malleability
|
||||
*
|
||||
* @param pubkeyScript
|
||||
* @param amount
|
||||
* @param feeRatePerKw
|
||||
* @return
|
||||
*/
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
|
||||
val promise = Promise[MakeFundingTxResponse]()
|
||||
(for {
|
||||
fundingTxResponse@MakeFundingTxResponseWithParent(parentTx, _, _, _) <- makeParentAndFundingTx(pubkeyScript, amount, feeRatePerKw)
|
||||
input0 = parentTx.txIn.head
|
||||
parentOfParentTx <- getTransaction(input0.outPoint.txid)
|
||||
_ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor")
|
||||
tempActor = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case WatchEventSpent(BITCOIN_OUTPUT_SPENT, spendingTx) =>
|
||||
if (parentTx.txid != spendingTx.txid) {
|
||||
// an input of our parent tx was spent by a tx that we're not aware of (i.e. a malleated version of our parent tx)
|
||||
// set a new watch; if it is confirmed, we'll use it as the new parent for our funding tx
|
||||
logger.warn(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}")
|
||||
}
|
||||
watcher ! WatchConfirmed(self, spendingTx.txid, spendingTx.txOut(0).publicKeyScript, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx))
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _) =>
|
||||
// a potential parent for our funding tx has been confirmed, let's update our funding tx
|
||||
val finalFundingTx = replaceParent(fundingTxResponse, tx)
|
||||
promise.success(MakeFundingTxResponse(finalFundingTx.fundingTx, finalFundingTx.fundingTxOutputIndex))
|
||||
}
|
||||
}))
|
||||
// we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar
|
||||
_ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, parentOfParentTx.txOut(input0.outPoint.index.toInt).publicKeyScript, BITCOIN_OUTPUT_SPENT)
|
||||
// and we publish the parent tx
|
||||
_ = logger.info(s"publishing parent tx: txid=${parentTx.txid} tx=${Transaction.write(parentTx)}")
|
||||
// we use a small delay so that we are sure Publish doesn't race with WatchSpent (which is ok but generates unnecessary warnings)
|
||||
_ = system.scheduler.scheduleOnce(100 milliseconds, watcher, PublishAsap(parentTx))
|
||||
} yield {}) onFailure {
|
||||
case t: Throwable => promise.failure(t)
|
||||
}
|
||||
promise.future
|
||||
}
|
||||
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
|
||||
FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, segwitChangeAddress, lockUnspents = true)
|
||||
// now let's sign the funding tx
|
||||
SignTransactionResponse(fundingTx, _) <- signTransaction(unsignedFundingTx)
|
||||
// there will probably be a change output, so we need to find which output is ours
|
||||
outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript)
|
||||
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
|
||||
} yield MakeFundingTxResponse(fundingTx, outputIndex)
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)
|
||||
.map(_ => true) // if bitcoind says OK, then we consider the tx succesfully published
|
||||
.recoverWith { case JsonRPCError(_) => getTransaction(tx.txid).map(_ => true).recover { case _ => false } } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
|
||||
.recoverWith { case JsonRPCError(e) =>
|
||||
logger.warn(s"txid=${tx.txid} error=$e")
|
||||
getTransaction(tx.txid).map(_ => true).recover { case _ => false } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
|
||||
}
|
||||
.recover { case _ => true } // in all other cases we consider that the tx has been published
|
||||
|
||||
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoint(tx.txIn.map(_.outPoint).toList) // we unlock all utxos used by the tx
|
||||
|
||||
/**
|
||||
* We currently only put a lock on the parent tx inputs, and we publish the parent tx immediately so there is nothing
|
||||
* to do here.
|
||||
*
|
||||
* @param tx
|
||||
* @return
|
||||
*/
|
||||
override def rollback(tx: Transaction): Future[Boolean] = Future.successful(true)
|
||||
}
|
||||
|
||||
object BitcoinCoreWallet {
|
||||
|
||||
case class Options(lockUnspents: Boolean)
|
||||
// @formatter:off
|
||||
case class Options(changeAddress: String, lockUnspents: Boolean)
|
||||
case class Utxo(txid: String, vout: Long)
|
||||
case class FundTransactionResponse(tx: Transaction, changepos: Int, feeSatoshis: Long)
|
||||
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
|
||||
// @formatter:on
|
||||
|
||||
|
||||
}
|
||||
@ -173,23 +173,18 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
})
|
||||
unspent <- rpcClient.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull))
|
||||
} yield ParallelGetResponse(awaiting.zip(txs.zip(unspent)).map(x => IndividualResult(x._1, x._2._1, x._2._2)))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the list of bitcoin addresses for which the wallet has UTXOs
|
||||
*/
|
||||
def listUnspentAddresses: Future[Seq[String]] = {
|
||||
import ExecutionContext.Implicits.global
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
|
||||
rpcClient.invoke("listunspent").collect {
|
||||
case JArray(values) => values.map(value => (value \ "address").extract[String])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*object Test extends App {
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import ExecutionContext.Implicits.global
|
||||
implicit val system = ActorSystem()
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
|
||||
val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
|
||||
user = "foo",
|
||||
password = "bar",
|
||||
host = "localhost",
|
||||
port = 28332))
|
||||
|
||||
println(Await.result(bitcoin_client.getTxBlockHash("dcb0abfa822402ce379fedd7bbbb2c824e53ef300313594c39282da1efd35f17"), 10 seconds))
|
||||
}*/
|
||||
|
||||
@ -128,9 +128,16 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
|
||||
|
||||
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
|
||||
|
||||
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, history), data) =>
|
||||
log.debug(s"scriptHash=$scriptHash has history=$history")
|
||||
val (heights1, pendingTransactionRequests1) = history.foldLeft((data.heights, data.pendingTransactionRequests)) {
|
||||
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, items), data) =>
|
||||
log.debug(s"scriptHash=$scriptHash has history=$items")
|
||||
val shadow_items = data.history.get(scriptHash) match {
|
||||
case Some(existing_items) => existing_items.filterNot(item => items.exists(_.tx_hash == item.tx_hash))
|
||||
case None => Nil
|
||||
}
|
||||
shadow_items.foreach(item => log.warning(s"keeping shadow item for txid=${item.tx_hash}"))
|
||||
val items0 = items ++ shadow_items
|
||||
|
||||
val (heights1, pendingTransactionRequests1) = items0.foldLeft((data.heights, data.pendingTransactionRequests)) {
|
||||
case ((heights, hashes), item) if !data.transactions.contains(item.tx_hash) && !data.pendingTransactionRequests.contains(item.tx_hash) =>
|
||||
// we retrieve the tx if we don't have it and haven't yet requested it
|
||||
client ! GetTransaction(item.tx_hash)
|
||||
@ -157,7 +164,7 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
|
||||
// no reorg, nothing to do
|
||||
}
|
||||
}
|
||||
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> history), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
|
||||
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> items0), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
|
||||
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
|
||||
|
||||
case Event(GetTransactionResponse(tx), data) =>
|
||||
@ -410,6 +417,9 @@ object ElectrumWallet {
|
||||
* @param heights transactions heights
|
||||
* @param history script hash -> history
|
||||
* @param locks transactions which lock some of our utxos.
|
||||
* @param pendingHistoryRequests requests pending a response from the electrum server
|
||||
* @param pendingTransactionRequests requests pending a response from the electrum server
|
||||
* @param pendingTransactions transactions received but not yet connected to their parents
|
||||
*/
|
||||
case class Data(tip: ElectrumClient.Header,
|
||||
accountKeys: Vector[ExtendedPrivateKey],
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import akka.stream.ActorMaterializer
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
|
||||
import org.json4s.JsonAST.{JArray, JInt, JValue}
|
||||
import org.json4s.{DefaultFormats, jackson}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class BitgoFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
import BitgoFeeProvider._
|
||||
|
||||
implicit val materializer = ActorMaterializer()
|
||||
val httpClient = Http(system)
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
override def getFeerates: Future[FeeratesPerByte] =
|
||||
for {
|
||||
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://www.bitgo.com/api/v1/tx/fee"), method = HttpMethods.GET))
|
||||
json <- Unmarshal(httpRes).to[JValue]
|
||||
feeRanges = parseFeeRanges(json)
|
||||
} yield extractFeerates(feeRanges)
|
||||
}
|
||||
|
||||
object BitgoFeeProvider {
|
||||
|
||||
case class BlockTarget(block: Int, fee: Long)
|
||||
|
||||
def parseFeeRanges(json: JValue): Seq[BlockTarget] = {
|
||||
val blockTargets = json \ "feeByBlockTarget"
|
||||
blockTargets.foldField(Seq.empty[BlockTarget]) {
|
||||
case (list, (strBlockTarget, JInt(feePerKb))) => list :+ BlockTarget(strBlockTarget.toInt, feePerKb.longValue() / 1024)
|
||||
}
|
||||
}
|
||||
|
||||
def extractFeerate(feeRanges: Seq[BlockTarget], maxBlockDelay: Int): Long = {
|
||||
// first we keep only fee ranges with a max block delay below the limit
|
||||
val belowLimit = feeRanges.filter(_.block <= maxBlockDelay)
|
||||
// out of all the remaining fee ranges, we select the one with the minimum higher bound
|
||||
belowLimit.map(_.fee).min
|
||||
}
|
||||
|
||||
def extractFeerates(feeRanges: Seq[BlockTarget]): FeeratesPerByte =
|
||||
FeeratesPerByte(
|
||||
block_1 = extractFeerate(feeRanges, 1),
|
||||
blocks_2 = extractFeerate(feeRanges, 2),
|
||||
blocks_6 = extractFeerate(feeRanges, 6),
|
||||
blocks_12 = extractFeerate(feeRanges, 12),
|
||||
blocks_36 = extractFeerate(feeRanges, 36),
|
||||
blocks_72 = extractFeerate(feeRanges, 72))
|
||||
|
||||
}
|
||||
@ -31,6 +31,14 @@ object Channel {
|
||||
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements
|
||||
val ANNOUNCEMENTS_MINCONF = 6
|
||||
|
||||
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
|
||||
val MAX_FUNDING_SATOSHIS = 16777216 // = 2^24
|
||||
val MIN_FUNDING_SATOSHIS = 1000
|
||||
val MAX_ACCEPTED_HTLCS = 483
|
||||
|
||||
// we don't want the counterparty to use a dust limit lower than that, because they wouldn't only hurt themselves we may need them to publish their commit tx in certain cases (backup/restore)
|
||||
val MIN_DUSTLIMIT = 546
|
||||
|
||||
case object TickRefreshChannelUpdate
|
||||
|
||||
}
|
||||
@ -135,7 +143,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
d match {
|
||||
case DATA_NORMAL(_, Some(shortChannelId), _, _, _) =>
|
||||
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId))
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
|
||||
relayer ! channelUpdate
|
||||
case _ => ()
|
||||
}
|
||||
@ -145,7 +153,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
|
||||
when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions {
|
||||
case Event(open: OpenChannel, DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(temporaryChannelId, localParams, _, remoteInit))) =>
|
||||
Try(Helpers.validateParamsFundee(temporaryChannelId, nodeParams, open.channelReserveSatoshis, open.fundingSatoshis, open.chainHash, open.feeratePerKw)) match {
|
||||
Try(Helpers.validateParamsFundee(nodeParams, open)) match {
|
||||
case Failure(t) =>
|
||||
log.warning(t.getMessage)
|
||||
val error = Error(open.temporaryChannelId, t.getMessage.getBytes)
|
||||
@ -197,7 +205,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
|
||||
when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
|
||||
case Event(accept: AcceptChannel, DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, localParams, _, remoteInit, _), open)) =>
|
||||
Try(Helpers.validateParamsFunder(temporaryChannelId, nodeParams, accept.channelReserveSatoshis, fundingSatoshis)) match {
|
||||
Try(Helpers.validateParamsFunder(nodeParams, open, accept)) match {
|
||||
case Failure(t) =>
|
||||
log.warning(t.getMessage)
|
||||
val error = Error(temporaryChannelId, t.getMessage.getBytes)
|
||||
|
||||
@ -10,6 +10,11 @@ import fr.acinq.eclair.UInt64
|
||||
class ChannelException(channelId: BinaryData, message: String) extends RuntimeException(message)
|
||||
// @formatter:off
|
||||
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
|
||||
case class InvalidChainHash (channelId: BinaryData, local: BinaryData, remote: BinaryData) extends ChannelException(channelId, s"invalid chain_hash (local=$local remote=$remote)")
|
||||
case class InvalidFundingAmount (channelId: BinaryData, fundingSatoshis: Long, min: Long, max: Long) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingSatoshis (min=$min max=$max)")
|
||||
case class InvalidPushAmount (channelId: BinaryData, pushMsat: Long, max: Long) extends ChannelException(channelId, s"invalid push_msat=$pushMsat (max=$max)")
|
||||
case class InvalidMaxAcceptedHtlcs (channelId: BinaryData, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
|
||||
case class InvalidDustLimit (channelId: BinaryData, dustLimitSatoshis: Long, min: Long) extends ChannelException(channelId, s"invalid dust_limit_satoshis=$dustLimitSatoshis (min=$min)")
|
||||
case class ChannelReserveTooHigh (channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
|
||||
case class ClosingInProgress (channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
|
||||
case class ClosingAlreadyInProgress (channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
|
||||
|
||||
@ -9,7 +9,7 @@ import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Scripts._
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, NodeParams}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
@ -38,21 +38,34 @@ object Helpers {
|
||||
case d: HasCommitments => d.channelId
|
||||
}
|
||||
|
||||
def validateParamsFunder(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long): Unit = {
|
||||
val reserveToFundingRatio = channelReserveSatoshis.toDouble / fundingSatoshis
|
||||
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) {
|
||||
throw new ChannelReserveTooHigh(temporaryChannelId, channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
|
||||
/**
|
||||
* Called by the fundee
|
||||
*/
|
||||
def validateParamsFundee(nodeParams: NodeParams, open: OpenChannel): Unit = {
|
||||
if (nodeParams.chainHash != open.chainHash) throw new InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)
|
||||
if (open.fundingSatoshis < Channel.MIN_FUNDING_SATOSHIS || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw new InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS)
|
||||
if (open.pushMsat > 1000 * open.fundingSatoshis) throw new InvalidPushAmount(open.temporaryChannelId, open.pushMsat, 1000 * open.fundingSatoshis)
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw new FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)
|
||||
// only enfore dust limit check on mainnet
|
||||
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
|
||||
if (open.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) throw new InvalidDustLimit(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUSTLIMIT)
|
||||
}
|
||||
val reserveToFundingRatio = open.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1)
|
||||
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw new ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
|
||||
}
|
||||
|
||||
def validateParamsFundee(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long, chainHash: BinaryData, initialFeeratePerKw: Long): Unit = {
|
||||
require(nodeParams.chainHash == chainHash, s"invalid chain hash $chainHash (we are on ${nodeParams.chainHash})")
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
// we are fundee => initialFeeratePerKw has been set by remote
|
||||
if (isFeeDiffTooHigh(initialFeeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) {
|
||||
throw new FeerateTooDifferent(temporaryChannelId, localFeeratePerKw, initialFeeratePerKw)
|
||||
/**
|
||||
* Called by the funder
|
||||
*/
|
||||
def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Unit = {
|
||||
if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) throw new InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)
|
||||
// only enfore dust limit check on mainnet
|
||||
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
|
||||
if (accept.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) throw new InvalidDustLimit(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUSTLIMIT)
|
||||
}
|
||||
validateParamsFunder(temporaryChannelId, nodeParams, channelReserveSatoshis, fundingSatoshis)
|
||||
val reserveToFundingRatio = accept.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1)
|
||||
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw new ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,7 +7,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.NodeParams
|
||||
import fr.acinq.eclair.blockchain.EclairWallet
|
||||
import fr.acinq.eclair.channel.HasCommitments
|
||||
import fr.acinq.eclair.channel.{Channel, HasCommitments}
|
||||
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
|
||||
import fr.acinq.eclair.router.Rebroadcast
|
||||
|
||||
@ -101,7 +101,10 @@ object Switchboard {
|
||||
def props(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Switchboard(nodeParams, watcher, router, relayer, wallet))
|
||||
|
||||
// @formatter:off
|
||||
case class NewChannel(fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelFlags: Option[Byte])
|
||||
case class NewChannel(fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelFlags: Option[Byte]) {
|
||||
require(fundingSatoshis.amount < Channel.MAX_FUNDING_SATOSHIS, s"fundingSatoshis must be less than ${Channel.MAX_FUNDING_SATOSHIS}")
|
||||
require(pushMsat.amount <= 1000 * fundingSatoshis.amount, s"pushMsat must be less or equal to fundingSatoshis")
|
||||
}
|
||||
case class NewConnection(remoteNodeId: PublicKey, address: InetSocketAddress, newChannel_opt: Option[NewChannel])
|
||||
// @formatter:on
|
||||
|
||||
|
||||
@ -64,4 +64,12 @@ package object eclair {
|
||||
def feerateByte2Kw(feeratePerByte: Long): Long = feeratePerByte * 1024 / 4
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param address bitcoin Base58 address
|
||||
* @return true if the address is a segwit address i.e. a p2sh-of-p2wpkh address.
|
||||
* We approximate this be returning true if the address is a p2sh address, there is no
|
||||
* way to tell what the script is.
|
||||
*/
|
||||
def isSegwitAddress(address: String) : Boolean = address.startsWith("2") || address.startsWith("3")
|
||||
}
|
||||
@ -17,14 +17,14 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin
|
||||
|
||||
def run(h2r: Map[BinaryData, (BinaryData, PaymentRequest)]): Receive = {
|
||||
|
||||
case ReceivePayment(amount, desc) =>
|
||||
case ReceivePayment(amount_opt, desc) =>
|
||||
Try {
|
||||
val paymentPreimage = randomBytes(32)
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
(paymentPreimage, paymentHash, PaymentRequest(nodeParams.chainHash, Some(amount), paymentHash, nodeParams.privateKey, desc))
|
||||
(paymentPreimage, paymentHash, PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc))
|
||||
} match {
|
||||
case Success((r, h, pr)) =>
|
||||
log.debug(s"generated payment request=${PaymentRequest.write(pr)} from amount=$amount")
|
||||
log.debug(s"generated payment request=${PaymentRequest.write(pr)} from amount=$amount_opt")
|
||||
sender ! pr
|
||||
context.become(run(h2r + (h -> (r, pr))))
|
||||
case Failure(t) =>
|
||||
|
||||
@ -12,7 +12,7 @@ import fr.acinq.eclair.wire._
|
||||
import scodec.Attempt
|
||||
|
||||
// @formatter:off
|
||||
case class ReceivePayment(amountMsat: MilliSatoshi, description: String)
|
||||
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String)
|
||||
case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, minFinalCltvExpiry: Long = PaymentLifecycle.defaultMinFinalCltvExpiry, maxAttempts: Int = 5)
|
||||
|
||||
sealed trait PaymentResult
|
||||
@ -181,16 +181,16 @@ object PaymentLifecycle {
|
||||
* - firstExpiry is the cltv expiry for the first htlc in the route
|
||||
* - a sequence of payloads that will be used to build the onion
|
||||
*/
|
||||
def buildPayloads(finalAmountMsat: Long, finalExpiry: Int, hops: Seq[PaymentHop]): (Long, Int, Seq[PerHopPayload]) =
|
||||
def buildPayloads(finalAmountMsat: Long, finalExpiry: Long, hops: Seq[PaymentHop]): (Long, Long, Seq[PerHopPayload]) =
|
||||
hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(0L, finalAmountMsat, finalExpiry) :: Nil)) {
|
||||
case ((msat, expiry, payloads), hop) =>
|
||||
(msat + hop.nextFee(msat), expiry + hop.cltvExpiryDelta, PerHopPayload(hop.shortChannelId, msat, expiry) +: payloads)
|
||||
}
|
||||
|
||||
// this is defined in BOLT 11
|
||||
val defaultMinFinalCltvExpiry = 9
|
||||
val defaultMinFinalCltvExpiry:Long = 9L
|
||||
|
||||
def buildCommand(finalAmountMsat: Long, finalExpiry: Int, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
|
||||
def buildCommand(finalAmountMsat: Long, finalExpiry: Long, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
|
||||
val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))
|
||||
val nodes = hops.map(_.nextNodeId)
|
||||
// BOLT 2 requires that associatedData == paymentHash
|
||||
|
||||
@ -26,7 +26,7 @@ import scala.util.Try
|
||||
*/
|
||||
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.Tag], signature: BinaryData) {
|
||||
|
||||
amount.map(a => require(a > MilliSatoshi(0) && a <= PaymentRequest.maxAmount, s"amount is not valid"))
|
||||
amount.map(a => require(a.amount > 0 && a.amount <= PaymentRequest.MAX_AMOUNT.amount, s"amount is not valid"))
|
||||
require(tags.collect { case _: PaymentRequest.PaymentHashTag => {} }.size == 1, "there must be exactly one payment hash tag")
|
||||
require(tags.collect { case PaymentRequest.DescriptionTag(_) | PaymentRequest.DescriptionHashTag(_) => {} }.size == 1, "there must be exactly one description tag or one description hash tag")
|
||||
|
||||
@ -102,7 +102,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
|
||||
object PaymentRequest {
|
||||
|
||||
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc
|
||||
val maxAmount = MilliSatoshi(4294967296L)
|
||||
val MAX_AMOUNT = MilliSatoshi(4294967296L)
|
||||
|
||||
def apply(chainHash: BinaryData, amount: Option[MilliSatoshi], paymentHash: BinaryData, privateKey: PrivateKey,
|
||||
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
|
||||
@ -279,6 +279,10 @@ object PaymentRequest {
|
||||
}
|
||||
}
|
||||
|
||||
case class UnknownTag(tag: Int5, int5s: Seq[Int5]) extends Tag {
|
||||
override def toInt5s = tag +: (writeSize(int5s.size) ++ int5s)
|
||||
}
|
||||
|
||||
object Amount {
|
||||
|
||||
/**
|
||||
@ -313,7 +317,7 @@ object PaymentRequest {
|
||||
}
|
||||
|
||||
object Tag {
|
||||
def parse(input: Seq[Byte]): Tag = {
|
||||
def parse(input: Seq[Int5]): Tag = {
|
||||
val tag = input(0)
|
||||
val len = input(1) * 32 + input(2)
|
||||
tag match {
|
||||
@ -345,6 +349,8 @@ object PaymentRequest {
|
||||
case c if c == Bech32.map('c') =>
|
||||
val expiry = readUnsignedLong(len, input.drop(3).take(len))
|
||||
MinFinalCltvExpiryTag(expiry)
|
||||
case _ =>
|
||||
UnknownTag(tag, input.drop(3).take(len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,7 +294,7 @@ object LightningMessageCodecs {
|
||||
("realm" | constant(ByteVector.fromByte(0))) ::
|
||||
("channel_id" | uint64) ::
|
||||
("amt_to_forward" | uint64) ::
|
||||
("outgoing_cltv_value" | int32) :: // we use a signed int32, it is enough to store cltv for 40 000 years
|
||||
("outgoing_cltv_value" | uint32) ::
|
||||
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload]
|
||||
|
||||
}
|
||||
|
||||
@ -157,4 +157,4 @@ case class ChannelUpdate(signature: BinaryData,
|
||||
|
||||
case class PerHopPayload(channel_id: Long,
|
||||
amtToForward: Long,
|
||||
outgoingCltvValue: Int)
|
||||
outgoingCltvValue: Long)
|
||||
11
eclair-core/src/test/resources/application.conf
Normal file
11
eclair-core/src/test/resources/application.conf
Normal file
@ -0,0 +1,11 @@
|
||||
akka {
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
loglevel = "DEBUG"
|
||||
|
||||
actor {
|
||||
debug {
|
||||
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
|
||||
fsm = on
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ object TestConstants {
|
||||
publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil,
|
||||
globalFeatures = "",
|
||||
localFeatures = "00",
|
||||
dustLimitSatoshis = 500,
|
||||
dustLimitSatoshis = 546,
|
||||
maxHtlcValueInFlightMsat = UInt64(150000000),
|
||||
maxAcceptedHtlcs = 100,
|
||||
expiryDeltaBlocks = 144,
|
||||
|
||||
@ -1,47 +1,104 @@
|
||||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import java.io.File
|
||||
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 com.typesafe.config.ConfigFactory
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.eclair.Kit
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import org.scalatest.FunSuite
|
||||
import fr.acinq.eclair.integration.IntegrationSpec
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuite, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext}
|
||||
import ExecutionContext.Implicits.global
|
||||
import scala.sys.process._
|
||||
|
||||
// this test is not run automatically
|
||||
class ExtendedBitcoinClientSpec extends FunSuite {
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
implicit lazy val system = ActorSystem()
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val config = ConfigFactory.load()
|
||||
val client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
|
||||
user = config.getString("eclair.bitcoind.rpcuser"),
|
||||
password = config.getString("eclair.bitcoind.rpcpassword"),
|
||||
host = config.getString("eclair.bitcoind.host"),
|
||||
port = 18332))
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
implicit val ec = ExecutionContext.Implicits.global
|
||||
val (chain, blockCount) = Await.result(client.rpcClient.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long])), 10 seconds)
|
||||
assert(chain == "test", "you should be on testnet")
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var client: ExtendedBitcoinClient = _
|
||||
|
||||
test("get transaction short id") {
|
||||
val txid = "7b2184f8539af648d51cc11d2a83630dd10fdf2a40a1824777d7f8da8e0d4b9e"
|
||||
val conf = Await.result(client.getTxConfirmations(txid), 5 seconds)
|
||||
val (height, index) = Await.result(client.getTransactionShortId(txid), 5 seconds)
|
||||
assert(height == 150002)
|
||||
assert(index == 7)
|
||||
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 BitcoinJsonRPCClient(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
|
||||
}
|
||||
}))
|
||||
client = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
}
|
||||
|
||||
test("get transaction by short id") {
|
||||
val tx = Await.result(client.getTransaction(150002, 7), 5 seconds)
|
||||
assert(tx.txid.toString() == "7b2184f8539af648d51cc11d2a83630dd10fdf2a40a1824777d7f8da8e0d4b9e")
|
||||
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()
|
||||
}
|
||||
|
||||
test("is tx output spendable") {
|
||||
val result = Await.result(client.isTransactionOuputSpendable("48ebfd0c0fe043b76eee09fcd8ea1e9248ffe1553fa30040fb7f7112ba3a202f", 0, true), 5 seconds)
|
||||
assert(result)
|
||||
val result1 = Await.result(client.isTransactionOuputSpendable("48ebfd0c0fe043b76eee09fcd8ea1e9248ffe1553fa30040fb7f7112ba3a202f", 5, true), 5 seconds)
|
||||
assert(!result1)
|
||||
test("list unspent addresss") {
|
||||
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](10 seconds)
|
||||
|
||||
val future = for {
|
||||
count <- client.getBlockCount
|
||||
_ = assert(count == 500)
|
||||
unspentAddresses <- client.listUnspentAddresses
|
||||
// coinbase txs need 100 confirmations to be spendable
|
||||
_ = assert(unspentAddresses.length == 500 - 100)
|
||||
// generate and import a new private key
|
||||
priv = PrivateKey("01" * 32)
|
||||
wif = Base58Check.encode(Base58.Prefix.SecretKeyTestnet, priv.toBin)
|
||||
_ = sender.send(bitcoincli, BitcoinReq("importprivkey", wif))
|
||||
_ = sender.expectMsgType[JValue](10 seconds)
|
||||
// send money to our private key
|
||||
address = Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, priv.publicKey.hash160)
|
||||
_ = client.sendFromAccount("", address, 1.0)
|
||||
_ = sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
_ = sender.expectMsgType[JValue](10 seconds)
|
||||
// and check that we find a utxo four our private key
|
||||
unspentAddresses1 <- client.listUnspentAddresses
|
||||
_ = assert(unspentAddresses1 contains address)
|
||||
} yield ()
|
||||
|
||||
Await.result(future, 10 seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoind
|
||||
|
||||
import java.io.File
|
||||
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 com.typesafe.config.ConfigFactory
|
||||
import fr.acinq.bitcoin.{MilliBtc, Satoshi, Script, Transaction}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.json4s.{DefaultFormats, JString}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process.{Process, _}
|
||||
|
||||
@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.14.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
|
||||
|
||||
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)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
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()
|
||||
// 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") {
|
||||
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)
|
||||
}
|
||||
|
||||
test("create/commit/rollback funding txes") {
|
||||
import collection.JavaConversions._
|
||||
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false))
|
||||
val config = ConfigFactory.load(commonConfig).getConfig("eclair")
|
||||
val bitcoinClient = new BitcoinJsonRPCClient(
|
||||
user = config.getString("bitcoind.rpcuser"),
|
||||
password = config.getString("bitcoind.rpcpassword"),
|
||||
host = config.getString("bitcoind.host"),
|
||||
port = config.getInt("bitcoind.rpcport"))
|
||||
val wallet = new BitcoinCoreWallet(bitcoinClient)
|
||||
|
||||
val sender = TestProbe()
|
||||
|
||||
wallet.getBalance.pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Satoshi] > Satoshi(0))
|
||||
|
||||
wallet.getFinalAddress.pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[String].startsWith("2"))
|
||||
|
||||
val fundingTxes = for (i <- 0 to 3) yield {
|
||||
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref)
|
||||
val MakeFundingTxResponse(fundingTx, _) = sender.expectMsgType[MakeFundingTxResponse]
|
||||
fundingTx
|
||||
}
|
||||
|
||||
sender.send(bitcoincli, BitcoinReq("listlockunspent"))
|
||||
assert(sender.expectMsgType[JValue](10 seconds).children.size === 4)
|
||||
|
||||
wallet.commit(fundingTxes(0)).pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Boolean])
|
||||
|
||||
wallet.rollback(fundingTxes(1)).pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Boolean])
|
||||
|
||||
wallet.commit(fundingTxes(2)).pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Boolean])
|
||||
|
||||
wallet.rollback(fundingTxes(3)).pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Boolean])
|
||||
|
||||
sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(0).txid.toString()))
|
||||
assert(sender.expectMsgType[JString](10 seconds).s === Transaction.write(fundingTxes(0)).toString())
|
||||
|
||||
sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString()))
|
||||
assert(sender.expectMsgType[JString](10 seconds).s === Transaction.write(fundingTxes(2)).toString())
|
||||
|
||||
// NB: bitcoin core doesn't clear the locks when a tx is published
|
||||
sender.send(bitcoincli, BitcoinReq("listlockunspent"))
|
||||
assert(sender.expectMsgType[JValue](10 seconds).children.size === 2)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -18,6 +18,7 @@ import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.Ignore
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
@ -28,6 +29,7 @@ import scala.concurrent.{Await, Future}
|
||||
import scala.sys.process.{Process, _}
|
||||
import scala.util.Random
|
||||
|
||||
@Ignore
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
@ -84,7 +86,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
|
||||
sender.expectMsgType[JValue](30 seconds)
|
||||
}
|
||||
|
||||
ignore("bitcoinj wallet commit") {
|
||||
test("bitcoinj wallet commit") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
@ -133,7 +135,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
|
||||
wallet.maybeCommitTx(tx2) // returns true! how come?
|
||||
}*/
|
||||
|
||||
ignore("manual publish/watch") {
|
||||
test("manual publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
@ -166,7 +168,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
|
||||
ignore("multiple publish/watch") {
|
||||
test("multiple publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.util.Timeout
|
||||
import org.json4s.DefaultFormats
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.Await
|
||||
|
||||
/**
|
||||
* Created by PM on 27/01/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitgoFeeProviderSpec extends FunSuite {
|
||||
|
||||
import BitgoFeeProvider._
|
||||
import org.json4s.jackson.JsonMethods.parse
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
val sample_response =
|
||||
"""
|
||||
{"feePerKb":136797,"cpfpFeePerKb":136797,"numBlocks":2,"confidence":80,"multiplier":1,"feeByBlockTarget":{"1":149453,"2":136797,"5":122390,"6":105566,"8":100149,"9":96254,"10":122151,"13":116855,"15":110860,"17":87402,"27":82635,"33":71098,"42":105782,"49":68182,"73":59207,"97":17336,"121":16577,"193":13545,"313":12268,"529":11122,"553":9139,"577":5395,"793":5070}}
|
||||
"""
|
||||
|
||||
test("parse test") {
|
||||
val json = parse(sample_response)
|
||||
val feeRanges = parseFeeRanges(json)
|
||||
assert(feeRanges.size === 23)
|
||||
}
|
||||
|
||||
test("extract fee for a particular block delay") {
|
||||
val json = parse(sample_response)
|
||||
val feeRanges = parseFeeRanges(json)
|
||||
val fee = extractFeerate(feeRanges, 6)
|
||||
assert(fee === 103)
|
||||
}
|
||||
|
||||
test("extract all fees") {
|
||||
val json = parse(sample_response)
|
||||
val feeRanges = parseFeeRanges(json)
|
||||
val feerates = extractFeerates(feeRanges)
|
||||
val ref = FeeratesPerByte(
|
||||
block_1 = 145,
|
||||
blocks_2 = 133,
|
||||
blocks_6 = 103,
|
||||
blocks_12 = 93,
|
||||
blocks_36 = 69,
|
||||
blocks_72 = 66)
|
||||
assert(feerates === ref)
|
||||
}
|
||||
|
||||
test("make sure API hasn't changed") {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
implicit val system = ActorSystem()
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
val provider = new BitgoFeeProvider()
|
||||
Await.result(provider.getFeerates, 10 seconds)
|
||||
}
|
||||
|
||||
}
|
||||
@ -83,7 +83,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
|
||||
if (stopping) {
|
||||
context stop self
|
||||
} else {
|
||||
paymentHandler ! ReceivePayment(MilliSatoshi(requiredAmount), "One coffee")
|
||||
paymentHandler ! ReceivePayment(Some(MilliSatoshi(requiredAmount)), "One coffee")
|
||||
context become waitingForPaymentRequest
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.{Hop, PaymentLifecycle}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, TestConstants}
|
||||
import fr.acinq.eclair.{Globals, NodeParams, TestConstants}
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
@ -29,7 +29,7 @@ trait StateTestsHelperMethods extends TestKitBase {
|
||||
router: TestProbe,
|
||||
relayer: TestProbe)
|
||||
|
||||
def init(): Setup = {
|
||||
def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams): Setup = {
|
||||
Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw))
|
||||
val alice2bob = TestProbe()
|
||||
val bob2alice = TestProbe()
|
||||
@ -37,8 +37,6 @@ trait StateTestsHelperMethods extends TestKitBase {
|
||||
val bob2blockchain = TestProbe()
|
||||
val relayer = TestProbe()
|
||||
val router = TestProbe()
|
||||
val nodeParamsA = TestConstants.Alice.nodeParams
|
||||
val nodeParamsB = TestConstants.Bob.nodeParams
|
||||
val wallet = new TestWallet
|
||||
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.id, alice2blockchain.ref, router.ref, relayer.ref))
|
||||
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.id, bob2blockchain.ref, router.ref, relayer.ref))
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
package fr.acinq.eclair.channel.states.a
|
||||
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL, _}
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, Error, Init, OpenChannel}
|
||||
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.Tag
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.duration._
|
||||
@ -20,7 +22,11 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp
|
||||
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val setup = init()
|
||||
val setup = if (test.tags.contains("mainnet")) {
|
||||
init(TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), TestConstants.Bob.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash))
|
||||
} else {
|
||||
init()
|
||||
}
|
||||
import setup._
|
||||
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
|
||||
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
|
||||
@ -42,6 +48,30 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp
|
||||
}
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (invalid max accepted htlcs)") { case (alice, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
// spec says max = 483
|
||||
val invalidMaxAcceptedHtlcs = 484
|
||||
alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs)
|
||||
val error = alice2bob.expectMsgType[Error]
|
||||
assert(error === Error(accept.temporaryChannelId, new InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (invalid dust limit)", Tag("mainnet")) { case (alice, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
// we don't want their dust limit to be below 546
|
||||
val lowDustLimitSatoshis = 545
|
||||
alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis)
|
||||
val error = alice2bob.expectMsgType[Error]
|
||||
assert(error === Error(accept.temporaryChannelId, new InvalidDustLimit(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUSTLIMIT).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (reserve too high)") { case (alice, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
@ -49,7 +79,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp
|
||||
val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong
|
||||
alice ! accept.copy(channelReserveSatoshis = reserveTooHigh)
|
||||
val error = alice2bob.expectMsgType[Error]
|
||||
assert(new String(error.data) === "channelReserveSatoshis too high: reserve=300000 fundingRatio=0.3 maxFundingRatio=0.05")
|
||||
assert(error === Error(accept.temporaryChannelId, new ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package fr.acinq.eclair.channel.states.a
|
||||
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
@ -40,6 +41,51 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper
|
||||
}
|
||||
}
|
||||
|
||||
test("recv OpenChannel (invalid chain)") { case (bob, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
// using livenet genesis block
|
||||
val livenetChainHash = Block.LivenetGenesisBlock.hash
|
||||
bob ! open.copy(chainHash = livenetChainHash)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(error === Error(open.temporaryChannelId, new InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv OpenChannel (funding too low)") { case (bob, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
val lowFundingMsat = 100
|
||||
bob ! open.copy(fundingSatoshis = lowFundingMsat)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv OpenChannel (funding too high)") { case (bob, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
val highFundingMsat = 100000000
|
||||
bob ! open.copy(fundingSatoshis = highFundingMsat)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv OpenChannel (invalid push_msat)") { case (bob, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
val invalidPushMsat = 100000000000L
|
||||
bob ! open.copy(pushMsat = invalidPushMsat)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(error === Error(open.temporaryChannelId, new InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, 1000 * open.fundingSatoshis).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv OpenChannel (reserve too high)") { case (bob, alice2bob, bob2alice, _) =>
|
||||
within(30 seconds) {
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
@ -47,7 +93,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper
|
||||
val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong
|
||||
bob ! open.copy(channelReserveSatoshis = reserveTooHigh)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(error === Error(open.temporaryChannelId, "channelReserveSatoshis too high: reserve=300000 fundingRatio=0.3 maxFundingRatio=0.05".getBytes("UTF-8")))
|
||||
assert(error === Error(open.temporaryChannelId, new ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage.getBytes("UTF-8")))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,6 +556,25 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
}
|
||||
}
|
||||
|
||||
test("recv CommitSig (only fee update)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
|
||||
sender.send(alice, CMD_UPDATE_FEE(TestConstants.feeratePerKw + 1000, commit = false))
|
||||
sender.expectMsg("ok")
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
|
||||
// actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes)
|
||||
alice2bob.expectMsgType[UpdateFee]
|
||||
alice2bob.forward(bob)
|
||||
alice2bob.expectMsgType[CommitSig]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[RevokeAndAck]
|
||||
bob2alice.forward(alice)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: maybe should be illegal?
|
||||
ignore("recv CommitSig (two htlcs received with same r)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
|
||||
within(30 seconds) {
|
||||
|
||||
@ -255,7 +255,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
||||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
@ -273,7 +273,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
@ -292,7 +292,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
@ -317,7 +317,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
@ -335,7 +335,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
@ -353,7 +353,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
|
||||
@ -9,7 +9,7 @@ import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
@ -74,7 +74,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
bitcoind.exitValue()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
@ -131,31 +131,25 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
}
|
||||
|
||||
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
|
||||
val eventListener1 = TestProbe()
|
||||
val eventListener2 = TestProbe()
|
||||
node1.system.eventStream.subscribe(eventListener1.ref, classOf[ChannelStateChanged])
|
||||
node2.system.eventStream.subscribe(eventListener2.ref, classOf[ChannelStateChanged])
|
||||
val sender = TestProbe()
|
||||
sender.send(node1.switchboard, NewConnection(
|
||||
remoteNodeId = node2.nodeParams.privateKey.publicKey,
|
||||
address = node2.nodeParams.publicAddresses.head,
|
||||
newChannel_opt = Some(NewChannel(Satoshi(fundingSatoshis), MilliSatoshi(pushMsat), None))))
|
||||
sender.expectMsgAnyOf(10 seconds, "connected", s"already connected to nodeId=${node2.nodeParams.privateKey.publicKey.toBin}")
|
||||
// funder transitions
|
||||
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_ACCEPT_CHANNEL)
|
||||
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_INTERNAL)
|
||||
// fundee transitions
|
||||
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_OPEN_CHANNEL)
|
||||
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CREATED)
|
||||
}
|
||||
|
||||
test("connect nodes") {
|
||||
//
|
||||
// A ---- B ---- C ---- D
|
||||
// | / \
|
||||
// --E--' F{1,2,3,4}
|
||||
// --E--' F{1,2,3,4,5}
|
||||
//
|
||||
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
|
||||
|
||||
connect(nodes("A"), nodes("B"), 10000000, 0)
|
||||
connect(nodes("B"), nodes("C"), 2000000, 0)
|
||||
connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
@ -167,51 +161,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
connect(nodes("C"), nodes("F4"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F5"), 5000000, 0)
|
||||
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
|
||||
val numberOfChannels = 10
|
||||
val channelEndpointsCount = 2 * numberOfChannels
|
||||
|
||||
// a channel has two endpoints
|
||||
val channelEndpointsCount = nodes.values.foldLeft(0) {
|
||||
case (sum, setup) =>
|
||||
sender.send(setup.register, 'channels)
|
||||
val channels = sender.expectMsgType[Map[BinaryData, ActorRef]]
|
||||
sum + channels.size
|
||||
}
|
||||
|
||||
// each funder sets up a WatchConfirmed on the parent tx, we need to make sure it has been received by the watcher
|
||||
var watches1 = Set.empty[Watch]
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
awaitCond({
|
||||
watches1 = nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
val watches = nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}
|
||||
watches1.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount / 2
|
||||
watches.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
// confirming the parent tx of the funding
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
within(30 seconds) {
|
||||
var count = 0
|
||||
while (count < channelEndpointsCount) {
|
||||
if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED) count = count + 1
|
||||
}
|
||||
}
|
||||
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
awaitCond({
|
||||
val watches2 = nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}
|
||||
(watches2 -- watches1).count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
|
||||
// confirming the funding tx
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
@ -255,7 +217,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
@ -273,7 +235,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
@ -292,7 +254,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
@ -317,7 +279,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
@ -335,7 +297,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
@ -353,7 +315,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
@ -371,6 +333,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
def scriptPubKeyToAddress(scriptPubKey: BinaryData) = Script.parse(scriptPubKey) match {
|
||||
case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)
|
||||
case OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash)
|
||||
case _ => ???
|
||||
}
|
||||
|
||||
@ -610,7 +574,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// first we send 3 mBTC to F so that it has a balance
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
@ -628,7 +592,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
||||
val localCommitTxF = sender.expectMsgType[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
// we now send some more money to F so that it creates a new commitment tx
|
||||
val amountMsat1 = MilliSatoshi(100000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat1, "1 coffee"))
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(Some(amountMsat1), "1 coffee"))
|
||||
val pr1 = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq1 = SendPayment(100000000L, pr1.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq1)
|
||||
|
||||
@ -24,7 +24,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
val amountMsat = MilliSatoshi(42000)
|
||||
sender.send(handler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, 0, "")
|
||||
@ -40,23 +40,34 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
// negative amount should fail
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(-50), "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(MilliSatoshi(-50)), "1 coffee"))
|
||||
val negativeError = sender.expectMsgType[Failure]
|
||||
assert(negativeError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// amount = 0 should fail
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(0), "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(MilliSatoshi(0)), "1 coffee"))
|
||||
val zeroError = sender.expectMsgType[Failure]
|
||||
assert(zeroError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// large amount should fail (> 42.95 mBTC)
|
||||
sender.send(handler, ReceivePayment(Satoshi(1) + PaymentRequest.maxAmount, "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(Satoshi(1) + PaymentRequest.MAX_AMOUNT), "1 coffee"))
|
||||
val largeAmountError = sender.expectMsgType[Failure]
|
||||
assert(largeAmountError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// success with 1 mBTC
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(100000000L), "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(MilliSatoshi(100000000L)), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.amount == Some(MilliSatoshi(100000000L)) && pr.nodeId.toString == Alice.nodeParams.privateKey.publicKey.toString)
|
||||
}
|
||||
|
||||
test("Payment request generation should succeed when the amount is not set") {
|
||||
val handler = system.actorOf(LocalPaymentHandler.props(Alice.nodeParams))
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
sender.send(handler, ReceivePayment(None, "This is a donation PR"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.amount == None && pr.nodeId.toString == Alice.nodeParams.privateKey.publicKey.toString)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ package fr.acinq.eclair.payment
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{Amount, ExtraHop, RoutingInfoTag}
|
||||
import fr.acinq.bitcoin.{Bech32, BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
@ -180,4 +180,30 @@ class PaymentRequestSpec extends FunSuite {
|
||||
val pr1 = PaymentRequest read serialized
|
||||
assert(pr.expiry === Some(21600))
|
||||
}
|
||||
|
||||
test("ignore unknown tags") {
|
||||
// create a new tag that we don't know about
|
||||
class MyExpiryTag(override val seconds: Long) extends ExpiryTag(seconds) {
|
||||
// replace the tag with 'j' which is not used yet
|
||||
override def toInt5s = super.toInt5s.updated(0, Bech32.map('j'))
|
||||
}
|
||||
|
||||
val pr = PaymentRequest(
|
||||
prefix = "lntb",
|
||||
amount = Some(MilliSatoshi(100000L)),
|
||||
timestamp = System.currentTimeMillis() / 1000L,
|
||||
nodeId = nodeId,
|
||||
tags = List(
|
||||
PaymentHashTag(BinaryData("01" * 32)),
|
||||
DescriptionTag("description"),
|
||||
new MyExpiryTag(42L)
|
||||
),
|
||||
signature = BinaryData.empty).sign(priv)
|
||||
|
||||
val serialized = PaymentRequest write pr
|
||||
val pr1 = PaymentRequest read serialized
|
||||
val Some(unknownTag) = pr1.tags.collectFirst { case u: UnknownTag => u }
|
||||
assert(unknownTag.tag == Bech32.map('j'))
|
||||
assert(unknownTag.toInt5s == (new MyExpiryTag(42L)).toInt5s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,11 +66,10 @@ object AnnouncementsBatchValidationSpec {
|
||||
val node2BitcoinKey = randomKey
|
||||
val amount = Satoshi(1000000)
|
||||
// first we publish the funding tx
|
||||
val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient, null)
|
||||
val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient)
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey)))
|
||||
val fundingTxFuture = wallet.makeParentAndFundingTx(fundingPubkeyScript, amount, 10000)
|
||||
val fundingTxFuture = wallet.makeFundingTx(fundingPubkeyScript, amount, 10000)
|
||||
val res = Await.result(fundingTxFuture, 10 seconds)
|
||||
Await.result(extendedBitcoinClient.publishTransaction(res.parentTx), 10 seconds)
|
||||
Await.result(extendedBitcoinClient.publishTransaction(res.fundingTx), 10 seconds)
|
||||
SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res.fundingTx, res.fundingTxOutputIndex)
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ class TransactionsSpec extends FunSuite {
|
||||
val remoteHtlcPriv = PrivateKey(BinaryData("eb" * 32), compressed = true)
|
||||
val localFinalPriv = PrivateKey(BinaryData("ff" * 32), compressed = true)
|
||||
val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(BinaryData("fe" * 32), compressed = true).publicKey))
|
||||
val localDustLimit = Satoshi(542)
|
||||
val localDustLimit = Satoshi(546)
|
||||
val toLocalDelay = 144
|
||||
val feeratePerKw = 1000
|
||||
|
||||
@ -141,7 +141,7 @@ class TransactionsSpec extends FunSuite {
|
||||
val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(BinaryData("a9" * 32), true).publicKey))
|
||||
val commitInput = Funding.makeFundingInputInfo(BinaryData("a0" * 32), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey)
|
||||
val toLocalDelay = 144
|
||||
val localDustLimit = Satoshi(542)
|
||||
val localDustLimit = Satoshi(546)
|
||||
val feeratePerKw = 22000
|
||||
|
||||
|
||||
|
||||
@ -198,7 +198,6 @@ class LightningMessageCodecsSpec extends FunSuite {
|
||||
|
||||
msgs.foreach {
|
||||
case msg => {
|
||||
println(msg)
|
||||
val encoded = lightningMessageCodec.encode(msg).require
|
||||
val decoded = lightningMessageCodec.decode(encoded).require
|
||||
assert(msg === decoded.value)
|
||||
|
||||
@ -15,17 +15,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
|
||||
@ -207,4 +207,12 @@
|
||||
}
|
||||
.button.copy-clipboard:pressed {
|
||||
-fx-background-color: rgb(220,222,225);
|
||||
}
|
||||
|
||||
/* --------- send modal ---------- */
|
||||
|
||||
.text-field.description-text, .text-area.description-text {
|
||||
-fx-background-color: rgb(235,235,235);
|
||||
-fx-text-fill: rgb(100,100,100);
|
||||
-fx-padding: 4px;
|
||||
}
|
||||
@ -30,8 +30,7 @@
|
||||
</VBox>
|
||||
<TextField fx:id="host" prefWidth="313.0" promptText="pubkey@host:port"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="0"/>
|
||||
<Label fx:id="hostError" opacity="0.0" styleClass="text-error, text-error-downward" text="Generic Invalid URI"
|
||||
mouseTransparent="true"
|
||||
<Label fx:id="hostError" styleClass="text-error, text-error-downward" mouseTransparent="true"
|
||||
GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
|
||||
|
||||
<CheckBox fx:id="simpleConnection" mnemonicParsing="false" text="Simple connection (no channel)"
|
||||
@ -55,8 +54,7 @@
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<Label fx:id="fundingSatoshisError" opacity="0.0" styleClass="text-error, text-error-downward"
|
||||
text="Generic Invalid Funding"
|
||||
<Label fx:id="fundingSatoshisError" styleClass="text-error, text-error-downward"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
|
||||
|
||||
<Label styleClass="text-muted" text="Optional Parameters" wrapText="true" GridPane.columnIndex="0"
|
||||
@ -71,10 +69,9 @@
|
||||
wrapText="true"/>
|
||||
</children>
|
||||
</VBox>
|
||||
<TextField fx:id="pushMsat" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<Label fx:id="pushMsatError" opacity="0.0" styleClass="text-error, text-error-downward"
|
||||
text="Generic Invalid Push"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="pushMsatField" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<Label fx:id="pushMsatError" styleClass="text-error, text-error-downward"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4" GridPane.columnSpan="2"/>
|
||||
<CheckBox fx:id="publicChannel" mnemonicParsing="true" selected="true" styleClass="text-sm"
|
||||
text="Public Channel"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="5"/>
|
||||
|
||||
@ -19,11 +19,12 @@
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="0">
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Amount to receive"/>
|
||||
<Label styleClass="text-strong" text="Optional amount to receive"/>
|
||||
<Label styleClass="label-description" wrapText="true" textAlignment="RIGHT"
|
||||
text="Maximum of ~0.042 BTC"/>
|
||||
</children>
|
||||
@ -52,11 +53,18 @@
|
||||
<TextArea fx:id="description" GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2"
|
||||
wrapText="true" prefHeight="50.0"/>
|
||||
|
||||
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="2" GridPane.columnIndex="0">
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Add the lightning: prefix"/>
|
||||
</children>
|
||||
</VBox>
|
||||
<CheckBox fx:id="prependPrefixCheckbox" selected="false" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
|
||||
|
||||
<Button defaultButton="true" mnemonicParsing="false" onAction="#handleGenerate" prefHeight="29.0"
|
||||
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel"
|
||||
text="Close"
|
||||
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="2" opacity="0"
|
||||
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="3" opacity="0"
|
||||
focusTraversable="false"/>
|
||||
|
||||
</children>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import java.net.URL?>
|
||||
<GridPane fx:id="nodeId" prefWidth="450.0" prefHeight="450.0" xmlns="http://javafx.com/javafx/8"
|
||||
<GridPane fx:id="nodeId" prefWidth="550.0" prefHeight="550.0" xmlns="http://javafx.com/javafx/8"
|
||||
xmlns:fx="http://javafx.com/fxml/1">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="110.0"/>
|
||||
@ -19,33 +19,47 @@
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="1.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Enter a Payment Request below" GridPane.columnSpan="2"
|
||||
GridPane.valignment="TOP"/>
|
||||
<TextArea fx:id="paymentRequest" minHeight="150.0" prefHeight="150.0" styleClass="ta" wrapText="true"
|
||||
GridPane.columnSpan="2" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS"/>
|
||||
<Label fx:id="paymentRequestError" opacity="0.0" text="Generic Invalid Payment Request" mouseTransparent="true"
|
||||
styleClass="text-error" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
|
||||
|
||||
<Label styleClass="text-muted" text="Amount (msat)" GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="amountField" focusTraversable="false" editable="false" styleClass="noteditable" text="0"
|
||||
<Label fx:id="paymentRequestError" mouseTransparent="true" styleClass="text-error" GridPane.columnSpan="2"
|
||||
GridPane.rowIndex="2"/>
|
||||
|
||||
<Label text="Node Id" GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="nodeIdField" focusTraversable="false" editable="false" styleClass="description-text"
|
||||
text="N/A"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
|
||||
<Label styleClass="text-muted" text="Node Id" GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="nodeIdField" focusTraversable="false" editable="false" styleClass="noteditable" text="N/A"
|
||||
<Label text="Payment Hash" GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="paymentHashField" focusTraversable="false" editable="false" styleClass="description-text"
|
||||
text="N/A"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
|
||||
<Label styleClass="text-muted" text="hash" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
|
||||
<TextField fx:id="hashField" focusTraversable="false" editable="false" styleClass="noteditable" text="N/A"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="5"/>
|
||||
<Label fx:id="descriptionLabel" text="Description" GridPane.halignment="RIGHT" GridPane.valignment="BASELINE"
|
||||
GridPane.rowIndex="5"/>
|
||||
<TextArea fx:id="descriptionField" focusTraversable="false" editable="false"
|
||||
styleClass="noteditable, description-text" text="N/A"
|
||||
prefHeight="80.0" maxHeight="80.0" GridPane.columnIndex="1" GridPane.rowIndex="5"/>
|
||||
|
||||
<Separator GridPane.columnSpan="2" GridPane.rowIndex="6"/>
|
||||
<Label text="Amount (msat)" GridPane.halignment="RIGHT" GridPane.valignment="BASELINE" GridPane.rowIndex="6"/>
|
||||
<VBox GridPane.columnIndex="1" GridPane.rowIndex="6">
|
||||
<children>
|
||||
<TextField fx:id="amountField"/>
|
||||
<Label fx:id="amountFieldError" mouseTransparent="true" styleClass="text-error"/>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
<Separator GridPane.columnSpan="2" GridPane.rowIndex="7"/>
|
||||
|
||||
<Button fx:id="sendButton" defaultButton="true" mnemonicParsing="false" onAction="#handleSend" text="Send"
|
||||
GridPane.rowIndex="7"/>
|
||||
GridPane.rowIndex="8"/>
|
||||
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
|
||||
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="7"/>
|
||||
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="8"/>
|
||||
</children>
|
||||
<styleClass>
|
||||
<String fx:value="grid"/>
|
||||
@ -53,5 +67,6 @@
|
||||
</styleClass>
|
||||
<stylesheets>
|
||||
<URL value="@../commons/globals.css"/>
|
||||
<URL value="@../main/main.css"/>
|
||||
</stylesheets>
|
||||
</GridPane>
|
||||
|
||||
@ -14,7 +14,7 @@ import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDiscon
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumConnected, ElectrumDisconnected}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.gui.controllers._
|
||||
import fr.acinq.eclair.gui.utils.CoinFormat
|
||||
import fr.acinq.eclair.gui.utils.CoinUtils
|
||||
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire.NodeAnnouncement
|
||||
@ -52,8 +52,8 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
|
||||
|
||||
def updateBalance(channelPaneController: ChannelPaneController, commitments: Commitments) = {
|
||||
val spec = commitments.localCommit.spec
|
||||
channelPaneController.capacity.setText(s"${CoinFormat.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.totalFunds)).amount)}")
|
||||
channelPaneController.amountUs.setText(s"${CoinFormat.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.toLocalMsat)).amount)}")
|
||||
channelPaneController.capacity.setText(s"${CoinUtils.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.totalFunds)).amount)}")
|
||||
channelPaneController.amountUs.setText(s"${CoinUtils.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.toLocalMsat)).amount)}")
|
||||
channelPaneController.balanceBar.setProgress(spec.toLocalMsat.toDouble / spec.totalFunds)
|
||||
}
|
||||
|
||||
|
||||
@ -86,9 +86,9 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte
|
||||
}
|
||||
}
|
||||
|
||||
def receive(amountMsat: MilliSatoshi, description: String): Future[String] = for {
|
||||
def receive(amountMsat_opt: Option[MilliSatoshi], description: String): Future[String] = for {
|
||||
kit <- fKit
|
||||
res <- (kit.paymentHandler ? ReceivePayment(amountMsat, description)).mapTo[PaymentRequest].map(PaymentRequest.write(_))
|
||||
res <- (kit.paymentHandler ? ReceivePayment(amountMsat_opt, description)).mapTo[PaymentRequest].map(PaymentRequest.write(_))
|
||||
} yield res
|
||||
|
||||
|
||||
|
||||
@ -7,33 +7,25 @@ import javafx.fxml.FXML
|
||||
import javafx.scene.control._
|
||||
import javafx.stage.Stage
|
||||
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.channel.ChannelFlags
|
||||
import fr.acinq.eclair.channel.{Channel, ChannelFlags}
|
||||
import fr.acinq.eclair.gui.Handlers
|
||||
import fr.acinq.eclair.gui.utils.GUIValidators
|
||||
import fr.acinq.eclair.gui.utils.{CoinUtils, GUIValidators}
|
||||
import fr.acinq.eclair.io.Switchboard.NewChannel
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Created by DPA on 23/09/2016.
|
||||
*/
|
||||
class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Logging {
|
||||
|
||||
/**
|
||||
* Funding must be less than {@code 2^24} satoshi.
|
||||
* PushMsat must not be greater than 1000 * Max funding
|
||||
*
|
||||
* https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
|
||||
*/
|
||||
val maxFunding = 16777216L
|
||||
val maxPushMsat = 1000L * maxFunding
|
||||
|
||||
@FXML var host: TextField = _
|
||||
@FXML var hostError: Label = _
|
||||
@FXML var simpleConnection: CheckBox = _
|
||||
@FXML var fundingSatoshis: TextField = _
|
||||
@FXML var fundingSatoshisError: Label = _
|
||||
@FXML var pushMsat: TextField = _
|
||||
@FXML var pushMsatField: TextField = _
|
||||
@FXML var pushMsatError: Label = _
|
||||
@FXML var publicChannel: CheckBox = _
|
||||
@FXML var unit: ComboBox[String] = _
|
||||
@ -45,44 +37,61 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo
|
||||
simpleConnection.selectedProperty.addListener(new ChangeListener[Boolean] {
|
||||
override def changed(observable: ObservableValue[_ <: Boolean], oldValue: Boolean, newValue: Boolean) = {
|
||||
fundingSatoshis.setDisable(newValue)
|
||||
pushMsat.setDisable(newValue)
|
||||
pushMsatField.setDisable(newValue)
|
||||
unit.setDisable(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@FXML def handleOpen(event: ActionEvent) = {
|
||||
clearErrors()
|
||||
if (GUIValidators.validate(host.getText, hostError, "Please use a valid url (pubkey@host:port)", GUIValidators.hostRegex)) {
|
||||
if (simpleConnection.isSelected) {
|
||||
handlers.open(host.getText, None)
|
||||
stage.close
|
||||
} else {
|
||||
if (GUIValidators.validate(fundingSatoshis.getText, fundingSatoshisError, "Funding must be numeric", GUIValidators.amountRegex)
|
||||
&& GUIValidators.validate(fundingSatoshisError, "Funding must be greater than 0", fundingSatoshis.getText.toLong > 0)) {
|
||||
val rawFunding = fundingSatoshis.getText.toLong
|
||||
val smartFunding = unit.getValue match {
|
||||
case "milliBTC" => Satoshi(rawFunding * 100000L)
|
||||
case "Satoshi" => Satoshi(rawFunding)
|
||||
case "milliSatoshi" => Satoshi(rawFunding / 1000L)
|
||||
}
|
||||
if (GUIValidators.validate(fundingSatoshisError, "Funding must be 16 777 216 satoshis (~0.167 BTC) or less", smartFunding.toLong < maxFunding)) {
|
||||
if (!pushMsat.getText.isEmpty) {
|
||||
// pushMsat is optional, so we validate field only if it isn't empty
|
||||
if (GUIValidators.validate(pushMsat.getText, pushMsatError, "Push msat must be numeric", GUIValidators.amountRegex)
|
||||
&& GUIValidators.validate(pushMsatError, "Push msat must be 16 777 216 000 msat (~0.167 BTC) or less", pushMsat.getText.toLong <= maxPushMsat)) {
|
||||
val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
|
||||
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(pushMsat.getText.toLong), Some(channelFlags))))
|
||||
stage.close
|
||||
}
|
||||
} else {
|
||||
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(0), None)))
|
||||
stage.close
|
||||
import fr.acinq.bitcoin._
|
||||
fundingSatoshis.getText match {
|
||||
case GUIValidators.amountDecRegex(_*) =>
|
||||
Try(CoinUtils.convertStringAmountToSat(fundingSatoshis.getText, unit.getValue)) match {
|
||||
case Success(capacitySat) if capacitySat.amount < 0 =>
|
||||
fundingSatoshisError.setText("Capacity must be greater than 0")
|
||||
case Success(capacitySat) if capacitySat.amount >= Channel.MAX_FUNDING_SATOSHIS =>
|
||||
fundingSatoshisError.setText(f"Capacity must be less than ${Channel.MAX_FUNDING_SATOSHIS}%,d sat")
|
||||
case Success(capacitySat) =>
|
||||
pushMsatField.getText match {
|
||||
case "" =>
|
||||
handlers.open(host.getText, Some(NewChannel(capacitySat, MilliSatoshi(0), None)))
|
||||
stage close()
|
||||
case GUIValidators.amountRegex(_*) =>
|
||||
Try(MilliSatoshi(pushMsatField.getText.toLong)) match {
|
||||
case Success(pushMsat) if pushMsat.amount > satoshi2millisatoshi(capacitySat).amount =>
|
||||
pushMsatError.setText("Push must be less or equal to capacity")
|
||||
case Success(pushMsat) =>
|
||||
val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
|
||||
handlers.open(host.getText, Some(NewChannel(capacitySat, pushMsat, Some(channelFlags))))
|
||||
stage close()
|
||||
case Failure(t) =>
|
||||
logger.error("Could not parse push amount", t)
|
||||
pushMsatError.setText("Push amount is not valid")
|
||||
}
|
||||
case _ => pushMsatError.setText("Push amount is not valid")
|
||||
}
|
||||
case Failure(t) =>
|
||||
logger.error("Could not parse capacity amount", t)
|
||||
fundingSatoshisError.setText("Capacity is not valid")
|
||||
}
|
||||
}
|
||||
case _ => fundingSatoshisError.setText("Capacity is not valid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def clearErrors() = {
|
||||
hostError.setText("")
|
||||
fundingSatoshisError.setText("")
|
||||
pushMsatError.setText("")
|
||||
}
|
||||
|
||||
@FXML def handleClose(event: ActionEvent) = stage.close
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package fr.acinq.eclair.gui.controllers
|
||||
import javafx.application.Platform
|
||||
import javafx.event.ActionEvent
|
||||
import javafx.fxml.FXML
|
||||
import javafx.scene.control.{ComboBox, Label, TextArea, TextField}
|
||||
import javafx.scene.control._
|
||||
import javafx.scene.image.{ImageView, WritableImage}
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.paint.Color
|
||||
@ -14,10 +14,11 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import com.google.zxing.{BarcodeFormat, EncodeHintType}
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.gui.Handlers
|
||||
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, GUIValidators}
|
||||
import fr.acinq.eclair.gui.utils.{CoinUtils, ContextMenuUtils, GUIValidators}
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
@ -29,6 +30,7 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage) extends
|
||||
@FXML var amountError: Label = _
|
||||
@FXML var unit: ComboBox[String] = _
|
||||
@FXML var description: TextArea = _
|
||||
@FXML var prependPrefixCheckbox: CheckBox = _
|
||||
|
||||
@FXML var resultBox: GridPane = _
|
||||
// the content of this field is generated and readonly
|
||||
@ -43,52 +45,77 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage) extends
|
||||
|
||||
@FXML def handleCopyInvoice(event: ActionEvent) = ContextMenuUtils.copyToClipboard(paymentRequestTextArea.getText)
|
||||
|
||||
/**
|
||||
* Generates a payment request from the amount/unit set in form. Displays an error if the generation fails.
|
||||
* Amount field content must obviously be numeric. It is also validated against minimal/maximal HTLC values.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
@FXML def handleGenerate(event: ActionEvent) = {
|
||||
if ((("milliBTC".equals(unit.getValue) || "Satoshi".equals(unit.getValue))
|
||||
&& GUIValidators.validate(amount.getText, amountError, "Amount must be numeric", GUIValidators.amountDecRegex))
|
||||
|| ("milliSatoshi".equals(unit.getValue) && GUIValidators.validate(amount.getText, amountError, "Amount must be numeric (no decimal msat)", GUIValidators.amountRegex))) {
|
||||
try {
|
||||
val Array(parsedInt, parsedDec) = if (amount.getText.contains(".")) amount.getText.split("\\.") else Array(amount.getText, "0")
|
||||
val amountDec = parsedDec.length match {
|
||||
case 0 => "000"
|
||||
case 1 => parsedDec.concat("00")
|
||||
case 2 => parsedDec.concat("0")
|
||||
case 3 => parsedDec
|
||||
case _ =>
|
||||
// amount has too many decimals, regex validation has failed somehow
|
||||
throw new NumberFormatException("incorrect amount")
|
||||
clearError()
|
||||
amount.getText match {
|
||||
case "" => createPaymentRequest(None)
|
||||
case GUIValidators.amountDecRegex(_*) =>
|
||||
Try(CoinUtils.convertStringAmountToMsat(amount.getText, unit.getValue)) match {
|
||||
case Success(amountMsat) if amountMsat.amount < 0 =>
|
||||
handleError("Amount must be greater than 0")
|
||||
case Success(amountMsat) if amountMsat.amount >= PaymentRequest.MAX_AMOUNT.amount =>
|
||||
handleError(f"Amount must be less than ${PaymentRequest.MAX_AMOUNT.amount}%,d msat (~${PaymentRequest.MAX_AMOUNT.amount / 1e11}%.3f BTC)")
|
||||
case Failure(_) =>
|
||||
handleError("Amount is incorrect")
|
||||
case Success(amountMsat) => createPaymentRequest(Some(amountMsat))
|
||||
}
|
||||
val smartAmount = unit.getValue match {
|
||||
case "milliBTC" => MilliSatoshi(parsedInt.toLong * 100000000L + amountDec.toLong * 100000L)
|
||||
case "Satoshi" => MilliSatoshi(parsedInt.toLong * 1000L + amountDec.toLong)
|
||||
case "milliSatoshi" => MilliSatoshi(amount.getText.toLong)
|
||||
}
|
||||
if (GUIValidators.validate(amountError, "Amount must be greater than 0", smartAmount.amount > 0)
|
||||
&& GUIValidators.validate(amountError, f"Amount must be less than ${PaymentRequest.maxAmount.amount}%,d msat (~${PaymentRequest.maxAmount.amount / 1e11}%.3f BTC)", smartAmount < PaymentRequest.maxAmount)
|
||||
&& GUIValidators.validate(amountError, "Description is too long, max 256 chars.", description.getText().size < 256)) {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
handlers.receive(smartAmount, description.getText) onComplete {
|
||||
case Success(s) =>
|
||||
Try(createQRCode(s)) match {
|
||||
case Success(wImage) => displayPaymentRequest(s, Some(wImage))
|
||||
case Failure(t) => displayPaymentRequest(s, None)
|
||||
}
|
||||
case Failure(t) => Platform.runLater(new Runnable {
|
||||
def run = GUIValidators.validate(amountError, "The payment request could not be generated", false)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: NumberFormatException =>
|
||||
logger.debug(s"Could not generate payment request for amount = ${amount.getText}")
|
||||
paymentRequestTextArea.setText("")
|
||||
amountError.setText("Amount is incorrect")
|
||||
amountError.setOpacity(1)
|
||||
}
|
||||
case _ => handleError("Amount must be a number")
|
||||
}
|
||||
}
|
||||
|
||||
private def displayPaymentRequest(pr: String, image: Option[WritableImage]) = Platform.runLater(new Runnable {
|
||||
/**
|
||||
* Display error message
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
private def handleError(message: String): Unit = {
|
||||
paymentRequestTextArea.setText("")
|
||||
amountError.setText(message)
|
||||
amountError.setOpacity(1)
|
||||
}
|
||||
|
||||
private def clearError(): Unit = {
|
||||
paymentRequestTextArea.setText("")
|
||||
amountError.setText("")
|
||||
amountError.setOpacity(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask eclair-core to create a Payment Request. If successful a QR code is generated and displayed, otherwise
|
||||
* an error message is shown.
|
||||
*
|
||||
* @param amount_opt optional amount of the payment request, in millisatoshi
|
||||
*/
|
||||
private def createPaymentRequest(amount_opt: Option[MilliSatoshi]) = {
|
||||
logger.debug(s"generate payment request for amount_opt=${amount_opt.getOrElse("N/A")} description=${description.getText()}")
|
||||
handlers.receive(amount_opt, description.getText) onComplete {
|
||||
case Success(s) =>
|
||||
val pr = if (prependPrefixCheckbox.isSelected) s"lightning:$s" else s
|
||||
Try(createQRCode(pr)) match {
|
||||
case Success(wImage) => displayPaymentRequestQR(pr, Some(wImage))
|
||||
case Failure(t) => displayPaymentRequestQR(pr, None)
|
||||
}
|
||||
case Failure(t) =>
|
||||
logger.error("Could not generate payment request", t)
|
||||
Platform.runLater(new Runnable {
|
||||
def run = GUIValidators.validate(amountError, "The payment request could not be generated", false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a QR Code from a QR code image.
|
||||
*
|
||||
* @param pr payment request described by the QR code
|
||||
* @param image QR code source image
|
||||
*/
|
||||
private def displayPaymentRequestQR(pr: String, image: Option[WritableImage]) = Platform.runLater(new Runnable {
|
||||
def run = {
|
||||
paymentRequestTextArea.setText(pr)
|
||||
if ("".equals(pr)) {
|
||||
|
||||
@ -8,8 +8,8 @@ import javafx.scene.input.KeyCode.{ENTER, TAB}
|
||||
import javafx.scene.input.KeyEvent
|
||||
import javafx.stage.Stage
|
||||
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.gui.Handlers
|
||||
import fr.acinq.eclair.gui.utils.GUIValidators
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
@ -24,12 +24,15 @@ class SendPaymentController(val handlers: Handlers, val stage: Stage) extends Lo
|
||||
@FXML var paymentRequest: TextArea = _
|
||||
@FXML var paymentRequestError: Label = _
|
||||
@FXML var nodeIdField: TextField = _
|
||||
@FXML var descriptionLabel: Label = _
|
||||
@FXML var descriptionField: TextArea = _
|
||||
@FXML var amountField: TextField = _
|
||||
@FXML var hashField: TextField = _
|
||||
@FXML var amountFieldError: Label = _
|
||||
@FXML var paymentHashField: TextField = _
|
||||
@FXML var sendButton: Button = _
|
||||
|
||||
@FXML def initialize(): Unit = {
|
||||
// ENTER or TAB events in the paymentRequest textarea insted fire or focus sendButton
|
||||
// ENTER or TAB events in the paymentRequest textarea instead fire or focus sendButton
|
||||
paymentRequest.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler[KeyEvent] {
|
||||
def handle(event: KeyEvent) = {
|
||||
event.getCode match {
|
||||
@ -45,33 +48,43 @@ class SendPaymentController(val handlers: Handlers, val stage: Stage) extends Lo
|
||||
})
|
||||
paymentRequest.textProperty.addListener(new ChangeListener[String] {
|
||||
def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String) = {
|
||||
clearErrors()
|
||||
Try(PaymentRequest.read(paymentRequest.getText)) match {
|
||||
case Success(pr) =>
|
||||
pr.amount.foreach(amount => amountField.setText(amount.amount.toString))
|
||||
pr.description match {
|
||||
case Left(s) => descriptionField.setText(s)
|
||||
case Right(hash) =>
|
||||
descriptionLabel.setText("Description's Hash")
|
||||
descriptionField.setText(hash.toString())
|
||||
}
|
||||
nodeIdField.setText(pr.nodeId.toString)
|
||||
hashField.setText(pr.paymentHash.toString)
|
||||
paymentHashField.setText(pr.paymentHash.toString)
|
||||
case Failure(f) =>
|
||||
GUIValidators.validate(paymentRequestError, "Please use a valid payment request", false)
|
||||
amountField.setText("0")
|
||||
nodeIdField.setText("N/A")
|
||||
hashField.setText("N/A")
|
||||
paymentRequestError.setText("Could not read this payment request")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@FXML def handleSend(event: ActionEvent) = {
|
||||
Try(PaymentRequest.read(paymentRequest.getText)) match {
|
||||
case Success(pr) =>
|
||||
Try(handlers.send(pr.nodeId, pr.paymentHash, pr.amount.get.amount, pr.minFinalCltvExpiry)) match {
|
||||
(Try(MilliSatoshi(amountField.getText().toLong)), Try(PaymentRequest.read(paymentRequest.getText))) match {
|
||||
case (Success(amountMsat), Success(pr)) =>
|
||||
Try(handlers.send(pr.nodeId, pr.paymentHash, amountMsat.amount, pr.minFinalCltvExpiry)) match {
|
||||
case Success(s) => stage.close
|
||||
case Failure(f) => GUIValidators.validate(paymentRequestError, s"Invalid Payment Request: ${f.getMessage}", false)
|
||||
case Failure(f) => paymentRequestError.setText(s"Invalid Payment Request: ${f.getMessage}")
|
||||
}
|
||||
case Failure(f) => GUIValidators.validate(paymentRequestError, "cannot parse payment request", false)
|
||||
case (_, Success(_)) => amountFieldError.setText("Invalid amount")
|
||||
case (_, Failure(f)) => paymentRequestError.setText("Could not read this payment request")
|
||||
}
|
||||
}
|
||||
|
||||
@FXML def handleClose(event: ActionEvent) = {
|
||||
stage.close
|
||||
}
|
||||
|
||||
private def clearErrors(): Unit = {
|
||||
paymentRequestError.setText("")
|
||||
amountFieldError.setText("")
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,8 @@ class SendPaymentStage(handlers: Handlers) extends Stage() with Logging {
|
||||
setTitle("Send a Payment Request")
|
||||
setMinWidth(450)
|
||||
setWidth(450)
|
||||
setMinHeight(450)
|
||||
setHeight(450)
|
||||
setMinHeight(550)
|
||||
setHeight(550)
|
||||
|
||||
// get fxml/controller
|
||||
val receivePayment = new FXMLLoader(getClass.getResource("/gui/modals/sendPayment.fxml"))
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
package fr.acinq.eclair.gui.utils
|
||||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
object CoinFormat {
|
||||
/**
|
||||
* Always 5 decimals
|
||||
*/
|
||||
val MILLI_BTC_PATTERN = "###,##0.00000"
|
||||
|
||||
/**
|
||||
* Localized formatter for milli-bitcoin amounts. Uses `MILLI_BTC_PATTERN`.
|
||||
*/
|
||||
val MILLI_BTC_FORMAT: DecimalFormat = new DecimalFormat(MILLI_BTC_PATTERN)
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package fr.acinq.eclair.gui.utils
|
||||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import grizzled.slf4j.Logging
|
||||
import fr.acinq.bitcoin._
|
||||
|
||||
object CoinUtils extends Logging {
|
||||
/**
|
||||
* Always 5 decimals
|
||||
*/
|
||||
val MILLI_BTC_PATTERN = "###,##0.00000"
|
||||
|
||||
/**
|
||||
* Localized formatter for milli-bitcoin amounts. Uses `MILLI_BTC_PATTERN`.
|
||||
*/
|
||||
val MILLI_BTC_FORMAT: DecimalFormat = new DecimalFormat(MILLI_BTC_PATTERN)
|
||||
|
||||
val MILLI_SATOSHI_LABEL = "milliSatoshi"
|
||||
val SATOSHI_LABEL = "satoshi"
|
||||
val MILLI_BTC_LABEL = "milliBTC"
|
||||
|
||||
/**
|
||||
* Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if
|
||||
* it has too many decimals because MilliSatoshi only accepts Long amount.
|
||||
*
|
||||
* @param amount numeric String, can be decimal.
|
||||
* @param unit bitcoin unit, can be milliSatoshi, Satoshi or milliBTC.
|
||||
* @return amount as a MilliSatoshi object.
|
||||
* @throws NumberFormatException if the amount parameter is not numeric.
|
||||
* @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC.
|
||||
*/
|
||||
@throws(classOf[NumberFormatException])
|
||||
@throws(classOf[IllegalArgumentException])
|
||||
def convertStringAmountToMsat(amount: String, unit: String): MilliSatoshi = {
|
||||
val amountDecimal = BigDecimal(amount)
|
||||
logger.debug(s"amount=$amountDecimal with unit=$unit")
|
||||
unit match {
|
||||
case MILLI_SATOSHI_LABEL => MilliSatoshi(amountDecimal.longValue())
|
||||
case SATOSHI_LABEL => MilliSatoshi((amountDecimal * 1000).longValue())
|
||||
case MILLI_BTC_LABEL => MilliSatoshi((amountDecimal * 1000 * 100000).longValue())
|
||||
case _ => throw new IllegalArgumentException("unknown unit")
|
||||
}
|
||||
}
|
||||
|
||||
def convertStringAmountToSat(amount: String, unit: String): Satoshi =
|
||||
millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit))
|
||||
}
|
||||
@ -10,7 +10,7 @@ import scala.util.matching.Regex
|
||||
object GUIValidators {
|
||||
val hostRegex = """([a-fA-F0-9]{66})@([a-zA-Z0-9:\.\-_]+):([0-9]+)""".r
|
||||
val amountRegex = """\d+""".r
|
||||
val amountDecRegex = """(\d+)|(\d+\.[\d]{1,3})""".r // accepts 3 decimals at most
|
||||
val amountDecRegex = """(\d+)|(\d+\.[\d]{1,})""".r
|
||||
val paymentRequestRegex =
|
||||
"""([a-zA-Z0-9]+):([a-zA-Z0-9]+):([a-zA-Z0-9]+)""".r
|
||||
val hexRegex = """[0-9a-fA-F]+""".r
|
||||
|
||||
@ -15,17 +15,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
|
||||
17
eclair-node/src/main/resources/application.conf
Normal file
17
eclair-node/src/main/resources/application.conf
Normal file
@ -0,0 +1,17 @@
|
||||
akka {
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
loglevel = "DEBUG"
|
||||
|
||||
actor {
|
||||
debug {
|
||||
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
|
||||
fsm = on
|
||||
}
|
||||
}
|
||||
|
||||
http {
|
||||
host-connection-pool {
|
||||
max-open-requests = 64
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration debug="true">
|
||||
<configuration>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.out</target>
|
||||
|
||||
43
pom.xml
43
pom.xml
@ -47,6 +47,9 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<!-- default tag used when building without git -->
|
||||
<git.commit.id>notag</git.commit.id>
|
||||
<git.commit.id.abbrev>notag</git.commit.id.abbrev>
|
||||
<scala.version>2.11.11</scala.version>
|
||||
<scala.version.short>2.11</scala.version.short>
|
||||
<akka.version>2.4.18</akka.version>
|
||||
@ -57,6 +60,11 @@
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
@ -65,7 +73,7 @@
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>2.2.2</version>
|
||||
<version>2.2.3</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.chrisdchristo</groupId>
|
||||
@ -80,38 +88,53 @@
|
||||
<artifactId>versions-maven-plugin</artifactId>
|
||||
<version>2.3</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>2.2.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<failOnNoGitDirectory>false</failOnNoGitDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>net.alchim31.maven</groupId>
|
||||
<artifactId>scala-maven-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<args>
|
||||
<args combine.children="append">
|
||||
<arg>-deprecation</arg>
|
||||
<arg>-feature</arg>
|
||||
<arg>-language:postfixOps</arg>
|
||||
<arg>-language:implicitConversions</arg>
|
||||
<arg>-Xfatal-warnings</arg>
|
||||
</args>
|
||||
<scalaCompatVersion>${scala.version.short}</scalaCompatVersion>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>process-resources</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>scalac</id>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<id>scaladoc</id>
|
||||
<goals>
|
||||
<goal>doc-jar</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<args>
|
||||
<arg>-no-link-warnings</arg>
|
||||
</args>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user