Compare commits

...

22 Commits

Author SHA1 Message Date
nicolas.dorier
43c3474282 Add docker-entrypoint and documentation for setting configuration 2017-12-08 23:19:43 +09:00
Nicolas Dorier
0794fb8d5a Docker support with repeatable build (#255)
Dependency to `git` has been removed, we now use `notag` when building without
a git directory.

In order to reliably fetch all dependencies, we do a first blank build
(with no source files), then we copy the sources and do a real commit.

This is a simpler and more robust approach.

Also, fixed the .dockerignore to filter out IDE files.
2017-12-05 17:29:47 +01:00
Pierre-Marie Padiou
5f6987bc1a Add test on signing a single Updatefee with no other updates (#254)
* Added a test on updatefee with no changes

* Fixed test logs: Akka slf4j logger was unset by 71f39a033b

* Closes #242 (can't reproduce)
2017-12-04 18:41:39 +01:00
Dominique
82c5b676e6
Unified cltv expiry type to Long (#253) 2017-12-04 17:37:11 +01:00
Dominique
71f39a033b Moved akka conf to eclair-node application.conf (#252)
Overriding configuration of dependencies must be done at the application level.
2017-12-04 13:31:05 +01:00
pm47
5b2fbb1bb2 re-added the '-no-link-warnings' option
But this time to `scaladoc` compiler.
2017-12-03 20:33:36 +01:00
pm47
1f36e8261e Revert "added '-no-link-warnings' compiler option"
This reverts commit 552fcd3a73.
2017-12-03 18:56:55 +01:00
pm47
552fcd3a73 added '-no-link-warnings' compiler option
This removes warning due to scaladoc, see:
http://www.scala-archive.org/Scaladoc-2-11-quot-throws-tag-quot-cannot-find-any-member-to-link-td4641850.html
2017-12-03 18:35:53 +01:00
pm47
1c7f66bf5b updated plugin versions 2017-12-03 18:05:47 +01:00
pm47
6e37586f9e disabled debug mode in default logback.xml 2017-12-01 18:42:50 +01:00
Dominique
b2c684a0da
Added the maven command to build eclair-node only (#250)
Also favouring `install` instead of `package`
2017-12-01 13:56:01 +01:00
Fabrice Drouin
84a9d73413 Disable dust limit checks on regtest and testnet (#249)
* disable dust limit checks on regtest and testnet

* fundee should also check dust limit they received open_channel (see #236)

* channel state tests: accept optional node parameters
use defaults if not provided
2017-12-01 11:18:16 +01:00
Dominique
44e7c3ba31 Handling optional amount in a Payment Request (#241)
* Enable generation of a payment request without amount

The amount field in a `PaymentRequest` was already optional but eclair
did not permit the generation of such a request.

Added a new `receive` service with no required amount field.

In the GUI, the parsing of the amount field and its conversion to
`MilliSatoshi` are reworked to better handle decimals.

* (gui) Amount's can be overriden when sending a payment request

The amount of a payment request can be changed and it is up to the
receiving node to accept or deny the payment according to its
implementation.

This also enables the user to pay through the GUI a payment request
where the amount has not been set, such as a donation. The amount is still
required!

The description field has also been added in the GUI. It is empty if the
description has not been set.

* (gui) Properly parse amounts from open channel form

* (gui) added optional `lightning:` scheme to payment request
2017-11-30 15:55:29 +01:00
Pierre-Marie Padiou
ffc4172e70
Added more checks on open/accept messages (#240)
We check that channel parameters are compliant with [BOLT 2](https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md) (this fixes #236).

We make sure that the counterparty chooses a decent `dust_limit`
because we want them to be able to publish their commitment, e.g. in a
data loss scenario. We also make sure that our configurable `dust_limit` isn't too low (this
fixes #234).

Also fixed our min `dust_limit` (542->546).

GUI:
* Using funding constants from `Channel`
* Also fixed an issue here `push` amount was compared to the max funding amount
  instead of the actual `funding` amount
2017-11-29 18:18:09 +01:00
Pierre-Marie Padiou
2ef479d38c
Fix concurrent spends with Electrum (#233)
By keeping temporary spending items in history
2017-11-29 17:40:07 +01:00
pm47
2d5d68bf3f a channel being restored is disabled 2017-11-28 13:34:35 +01:00
Pierre-Marie Padiou
ba25821e85 eclair-cli: added payreq desc, fixed method names (#238) 2017-11-27 20:46:17 +01:00
Fabrice Drouin
86133db5f7 Check that all UTXOS are p2sh on testnet (#228)
To avoid malleability issues, ask users to only have p2sh-of-p2wkh outputs.
on testnet, on startup we check that all UTXOs are p2sh (we cannot check that the
p2sh script is a p2wpkh script). It is not needed on regtest since there is no
chance that wallet tx will be malleated.
2017-11-27 18:30:59 +01:00
Pierre-Marie Padiou
40b18aed8b Added bitgo fee provider (#237)
* added bitgo fee provider and set it as default, keeping `earn.com` as fallback.
2017-11-27 18:17:38 +01:00
Pierre-Marie Padiou
8b151eb5c0
Removed bitcoin-core parent-tx hack (#231)
This was a workaround because bitcoin-core could produce malleable funding
transactions.

We now:
1) assume that all existing funds in bitcoin core are in segwit P2S addresses
2) manually create segwit change addresses when we create new transactions

Also disabled unused/unreliable bitcoinj tests
2017-11-27 15:05:25 +01:00
Pierre-Marie Padiou
6304041d77
Use tx-output pubkeyscript as hint for electrum watcher (#232)
* use tx-output pubkeyscript as hint for electrum watcher to detect tx confirmed

* support adding a WatchConfirmed to a tx that has no output
2017-11-27 12:28:44 +01:00
Fabrice Drouin
bf0423c020 Ignore unknown tags when parsing a payment request
This fixes #206.
2017-11-24 11:30:36 +01:00
61 changed files with 1110 additions and 545 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
Dockerfile
.dockerignore
.git
**/*.idea
**/*.iml
**/target

View File

@ -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`

56
Dockerfile Normal file
View File

@ -0,0 +1,56 @@
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.jar
ENV ECLAIR_DATADIR=/data
ENV JAVA_OPTS=
RUN mkdir -p "$ECLAIR_DATADIR"
VOLUME [ "/data" ]
ENTRYPOINT java $JAVA_OPTS -Declair.datadir=$ECLAIR_DATADIR -jar eclair-node.jar

View File

@ -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
@ -133,6 +140,23 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey
help | | display available methods
## Docker
A [Dockerfile](Dockerfile) images are built on each commit on [docker hub](https://hub.docker.com/r/ACINQ/eclair) for running a dockerized eclair-node.
You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`.
```
docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq\eclair
```
If you want to persist the data directory, you can make the volume to your host with the `-v` argument, as the following example:
```
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq\eclair
```
## Resources
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell

View File

@ -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"

View File

@ -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>

View File

@ -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
}
}
}

View File

@ -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"),

View File

@ -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")

View File

@ -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) =>

View File

@ -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) =>

View File

@ -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
}

View File

@ -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))
}*/

View File

@ -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],

View File

@ -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))
}

View File

@ -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)

View File

@ -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")

View File

@ -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)
}
/**

View File

@ -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

View File

@ -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")
}

View File

@ -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) =>

View File

@ -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

View File

@ -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))
}
}
}

View File

@ -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]
}

View File

@ -157,4 +157,4 @@ case class ChannelUpdate(signature: BinaryData,
case class PerHopPayload(channel_id: Long,
amtToForward: Long,
outgoingCltvValue: Int)
outgoingCltvValue: Long)

View 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
}
}
}

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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>

View File

@ -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;
}

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)) {

View File

@ -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("")
}
}

View File

@ -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"))

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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

View File

@ -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>

View 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
}
}
}

View File

@ -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
View File

@ -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>