From 928f73d47026fafe993097c583c44d76311672a5 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 14 Dec 2018 15:49:07 +0100 Subject: [PATCH 01/75] integration tests: upgrade to bitcoin 0.17.1 --- eclair-core/pom.xml | 18 +++++++++--------- .../bitcoind/BitcoinCoreWallet.scala | 15 ++++++++++++--- .../test/resources/integration/bitcoin.conf | 4 +++- .../bitcoind/BitcoinCoreWalletSpec.scala | 12 ++++++++---- .../blockchain/bitcoind/BitcoindService.scala | 13 ++++++++++--- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 5bbd7ac664..ee39d48cce 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -79,10 +79,10 @@ true - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-x86_64-linux-gnu.tar.gz - c371e383f024c6c45fb255d528a6beec - e6d8ab1f7661a6654fd81e236b9b5fd35a3d4dcb + 724043999e2b5ed0c088e8db34f15d43 + 546ee35d4089c7ccc040a01cdff3362599b8bc53 @@ -93,10 +93,10 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-osx64.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-osx64.tar.gz - bacd87d0c3f65a5acd666e33d094a59e - 62cc5bd9ced610bb9e8d4a854396bfe2139e3d0f + b5a792c6142995faa42b768273a493bd + 8bd51c7024d71de07df381055993e9f472013db8 @@ -107,9 +107,9 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-win64.zip - bbde9b1206956d19298034319e9f405e - 85e3dc8a9c6f93b1b20cb79fa5850b5ce81da221 + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-win64.zip + b0e824e9dd02580b5b01f073f3c89858 + 4e17bad7d08c465b444143a93cd6eb1c95076e3f diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index d608e92c8f..7843343f60 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -19,10 +19,12 @@ package fr.acinq.eclair.blockchain.bitcoind import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, JsonRPCError} import fr.acinq.eclair.transactions.Transactions import grizzled.slf4j.Logging +import org.json4s.DefaultFormats import org.json4s.JsonAST._ +import org.json4s.jackson.Serialization import scala.concurrent.{ExecutionContext, Future} @@ -49,6 +51,13 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC rpcClient.invoke("signrawtransaction", hex).map(json => { val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" + if (!complete) { + val message = json \ "errors" match { + case value: JValue => Serialization.write(value)(DefaultFormats) + case _ => "signrawtransaction failed" + } + throw new JsonRPCError(Error(-1, message)) + } SignTransactionResponse(Transaction.read(hex), complete) }) @@ -71,7 +80,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = { val f = signTransaction(tx) - // if signature fails (e.g. because wallet is uncrypted) we need to unlock the utxos + // if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos f.recoverWith { case _ => unlockOutpoints(tx.txIn.map(_.outPoint)) .recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure @@ -91,7 +100,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC // we ask bitcoin core to add inputs to the funding tx, and use the specified change address FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw) // now let's sign the funding tx - SignTransactionResponse(fundingTx, _) <- signTransactionOrUnlock(unsignedFundingTx) + SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx) // there will probably be a change output, so we need to find which output is ours outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee") diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index da4dd59a07..6ef9572c9d 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -1,7 +1,8 @@ regtest=1 +noprinttoconsole=1 server=1 port=28333 -rpcport=28332 +regtest.rpcport=28332 rpcuser=foo rpcpassword=bar txindex=1 @@ -9,3 +10,4 @@ zmqpubrawblock=tcp://127.0.0.1:28334 zmqpubrawtx=tcp://127.0.0.1:28335 rpcworkqueue=64 addresstype=bech32 +deprecatedrpc=signrawtransaction diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 4acb1255d9..8701639b8a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -141,10 +141,10 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString())) assert(sender.expectMsgType[JString](10 seconds).s === fundingTxes(2).toString()) - // NB: bitcoin core doesn't clear the locks when a tx is published + // NB: bitcoin core before 0.17.0 doesn't clear the locks when a tx is published sender.send(bitcoincli, BitcoinReq("listlockunspent")) - assert(sender.expectMsgType[JValue](10 seconds).children.size === 2) - + val expectedLocks = if (bitcoinVersion >= 170000) 0 else 2 + assert(sender.expectMsgType[JValue](10 seconds).children.size === expectedLocks) } test("encrypt wallet") { @@ -177,7 +177,11 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref) - assert(sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error.message.contains("Please enter the wallet passphrase with walletpassphrase first.")) + val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error + // behaviour has changed, bitcoin core will now return a more generic error + if (bitcoinVersion < 170000) { + assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first.")) + } sender.send(bitcoincli, BitcoinReq("listlockunspent")) assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 8c9cc28d2b..7d9971c34b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -28,7 +28,7 @@ import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec import grizzled.slf4j.Logging -import org.json4s.JsonAST.JValue +import org.json4s.JsonAST.{JInt, JValue} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -44,12 +44,13 @@ trait BitcoindService extends Logging { val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}" logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.3/bin/bitcoind") + val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.17.1/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null var bitcoinrpcclient: BitcoinJsonRPCClient = null var bitcoincli: ActorRef = null + var bitcoinVersion: Int = _ case class BitcoinReq(method: String, params: Any*) @@ -83,7 +84,13 @@ trait BitcoindService extends Logging { logger.info(s"waiting for bitcoind to initialize...") awaitCond({ sender.send(bitcoincli, BitcoinReq("getnetworkinfo")) - sender.receiveOne(5 second).isInstanceOf[JValue] + sender.receiveOne(5 second) match { + case info: JValue => + val JInt(version) = info \ "version" + bitcoinVersion = version.intValue() + true + case _ => false + } }, max = 30 seconds, interval = 500 millis) logger.info(s"generating initial blocks...") sender.send(bitcoincli, BitcoinReq("generate", 500)) From 65bbab9bf8d9c0d5e2ac0e20f4ba07f3f8297327 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 12 Feb 2019 17:50:09 +0100 Subject: [PATCH 02/75] Bitcoin RPC: use `signrawtransactionwithwallet` We don't use `signrawtransaction` anymore, which was deprecated in Bitcoin Core 0.17 and will be removed in 0.18. This means that we don't support 0.16.3 and older. --- .../eclair/blockchain/bitcoind/BitcoinCoreWallet.scala | 7 ++++--- .../src/test/resources/integration/bitcoin.conf | 4 ++-- .../blockchain/bitcoind/BitcoinCoreWalletSpec.scala | 10 +++------- .../bitcoind/ExtendedBitcoinClientSpec.scala | 4 ++-- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 7843343f60..d3a27696bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -48,13 +48,14 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toString(), lockUnspents, feeRatePerKw) def signTransaction(hex: String): Future[SignTransactionResponse] = - rpcClient.invoke("signrawtransaction", hex).map(json => { + rpcClient.invoke("signrawtransactionwithwallet", hex).map(json => { val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" if (!complete) { val message = json \ "errors" match { - case value: JValue => Serialization.write(value)(DefaultFormats) - case _ => "signrawtransaction failed" + case value: JValue => + Serialization.write(value)(DefaultFormats) + case _ => "signrawtransactionwithwallet failed" } throw new JsonRPCError(Error(-1, message)) } diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index 6ef9572c9d..29775744a1 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -2,7 +2,6 @@ regtest=1 noprinttoconsole=1 server=1 port=28333 -regtest.rpcport=28332 rpcuser=foo rpcpassword=bar txindex=1 @@ -10,4 +9,5 @@ zmqpubrawblock=tcp://127.0.0.1:28334 zmqpubrawtx=tcp://127.0.0.1:28335 rpcworkqueue=64 addresstype=bech32 -deprecatedrpc=signrawtransaction +[regtest] +rpcport=28332 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 8701639b8a..b73a187095 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -141,10 +141,9 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString())) assert(sender.expectMsgType[JString](10 seconds).s === fundingTxes(2).toString()) - // NB: bitcoin core before 0.17.0 doesn't clear the locks when a tx is published + // NB: from 0.17.0 on bitcoin core will clear locks when a tx is published sender.send(bitcoincli, BitcoinReq("listlockunspent")) - val expectedLocks = if (bitcoinVersion >= 170000) 0 else 2 - assert(sender.expectMsgType[JValue](10 seconds).children.size === expectedLocks) + assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) } test("encrypt wallet") { @@ -178,10 +177,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref) val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error - // behaviour has changed, bitcoin core will now return a more generic error - if (bitcoinVersion < 170000) { - assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first.")) - } + assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first")) sender.send(bitcoincli, BitcoinReq("listlockunspent")) assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index d730b2bf13..0b6c7ac335 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -67,7 +67,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val json = sender.expectMsgType[JValue] val JString(unsignedtx) = json \ "hex" val JInt(changePos) = json \ "changepos" - bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" val tx = Transaction.read(signedTx) val txid = tx.txid.toString() @@ -92,7 +92,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val pos = if (changePos == 0) 1 else 0 bitcoinClient.invoke("createrawtransaction", Array(Map("txid" -> txid, "vout" -> pos)), Map(address -> 5.99999)).pipeTo(sender.ref) val JString(unsignedtx) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" signedTx } From 3f8f328f70a8aa9d6df41b8a8ccb226544f874e5 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 25 Feb 2019 09:38:10 +0100 Subject: [PATCH 03/75] Remove unused travis files We download bitcoin core and check with maven now. --- travis/builddeps.sh | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100755 travis/builddeps.sh diff --git a/travis/builddeps.sh b/travis/builddeps.sh deleted file mode 100755 index bfff183d5f..0000000000 --- a/travis/builddeps.sh +++ /dev/null @@ -1,25 +0,0 @@ -pushd . -# lightning deps -sudo add-apt-repository -y ppa:chris-lea/libsodium -sudo apt-get update -sudo apt-get install -y libsodium-dev libgmp-dev libsqlite3-dev -cd -git clone https://github.com/luke-jr/libbase58.git -cd libbase58 -./autogen.sh && ./configure && make && sudo make install -# lightning -cd -git clone https://github.com/ElementsProject/lightning.git -cd lightning -git checkout fce9ee29e3c37b4291ebb050e6a687cfaa7df95a -git submodule init -git submodule update -make -# bitcoind -cd -wget https://bitcoin.org/bin/bitcoin-core-0.13.0/bitcoin-0.13.0-x86_64-linux-gnu.tar.gz -echo "bcc1e42d61f88621301bbb00512376287f9df4568255f8b98bc10547dced96c8 bitcoin-0.13.0-x86_64-linux-gnu.tar.gz" > sha256sum.asc -sha256sum -c sha256sum.asc -tar xzvf bitcoin-0.13.0-x86_64-linux-gnu.tar.gz -popd - From e9fb058345cb921db9a511d754fa9b389ab5560d Mon Sep 17 00:00:00 2001 From: sstone Date: Sat, 2 Mar 2019 15:19:42 +0100 Subject: [PATCH 04/75] Bitcoin Wallet: cleaner handling of signtransaction errors --- .../bitcoind/BitcoinCoreWallet.scala | 6 +--- .../bitcoind/BitcoinCoreWalletSpec.scala | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index d3a27696bb..5723a177ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -52,11 +52,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" if (!complete) { - val message = json \ "errors" match { - case value: JValue => - Serialization.write(value)(DefaultFormats) - case _ => "signrawtransactionwithwallet failed" - } + val message = (json \ "errors" \\ classOf[JString]).mkString(",") throw new JsonRPCError(Error(-1, message)) } SignTransactionResponse(Transaction.read(hex), complete) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index b73a187095..d077646ae7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,7 +21,7 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script} +import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script, Transaction, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} @@ -94,6 +94,32 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe } } + test("handle error when signing transactions") { + val bitcoinClient = new BasicBitcoinJsonRPCClient( + 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() + + // create and fund a transaction + wallet.getFinalAddress.pipeTo(sender.ref) + val address = sender.expectMsgType[String] + val unsignedTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0) + wallet.fundTransaction(unsignedTx, false, 1500).pipeTo(sender.ref) + val FundTransactionResponse(fundedTx, _, _) = sender.expectMsgType[FundTransactionResponse] + + // change the index of the UTXO that it spends + val fundedTx1 = fundedTx.copy(txIn = fundedTx.txIn.updated(0, fundedTx.txIn(0).copy(outPoint = fundedTx.txIn(0).outPoint.copy(index = fundedTx.txIn(0).outPoint.index + 1)))) + + // signing it should fail, and the error message should contain the txid of the UTXO that could not be used + wallet.signTransaction(fundedTx1).pipeTo(sender.ref) + val Failure(JsonRPCError(error)) = sender.expectMsgType[Failure] + assert(error.message.contains(fundedTx1.txIn(0).outPoint.txid.toString())) + } + test("create/commit/rollback funding txes") { val bitcoinClient = new BasicBitcoinJsonRPCClient( user = config.getString("bitcoind.rpcuser"), From de83752feb8e57c42803dddc291a13ac146670d8 Mon Sep 17 00:00:00 2001 From: sstone Date: Sun, 3 Mar 2019 20:35:11 +0100 Subject: [PATCH 05/75] Check that bitcoind version is 0.17.0 or higher Plus minor code reformatting for some tests. --- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 4 +--- .../blockchain/bitcoind/BitcoinCoreWalletSpec.scala | 9 ++++++++- .../blockchain/bitcoind/ExtendedBitcoinClientSpec.scala | 9 ++++++++- .../blockchain/fee/BitcoinCoreFeeProviderSpec.scala | 9 ++++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index a9ea3d376c..39eab054a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -126,15 +126,13 @@ class Setup(datadir: File, } yield (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) // blocking sanity checks val (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") - assert(bitcoinVersion >= 160300, "Eclair requires Bitcoin Core 0.16.3 or higher") + assert(bitcoinVersion >= 170000, "Eclair requires Bitcoin Core 0.17.0 or higher") assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") if (chainHash != Block.RegtestGenesisBlock.hash) { assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Make sure that all your UTXOS are segwit UTXOS and not p2pkh (check out our README for more details)") } assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress") assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks") - // TODO: add a check on bitcoin version? - Bitcoind(bitcoinClient) case ELECTRUM => val addresses = config.hasPath("electrum") match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index d077646ae7..f1a5842a52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -41,7 +41,14 @@ import scala.util.{Random, Try} class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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 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.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index 0b6c7ac335..18a08923de 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -34,7 +34,14 @@ import scala.concurrent.ExecutionContext.Implicits.global class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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 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.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") implicit val formats = DefaultFormats diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 82c292166c..7b9126b2ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -38,7 +38,14 @@ import scala.util.Random class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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 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.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString From 31513605953f3bd58ed0958697ed0bc951a89911 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 6 Mar 2019 10:17:41 +0100 Subject: [PATCH 06/75] BitcoinCoreWallet: add signing tests with multiple bad inputs Check that we handle errors properly when signrawtransactionwithwallet returns multiple errors. --- .../bitcoind/BitcoinCoreWalletSpec.scala | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index f1a5842a52..c726e4b226 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,7 +21,7 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script, Transaction, TxOut} +import fr.acinq.bitcoin.{BinaryData, Block, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} @@ -101,7 +101,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe } } - test("handle error when signing transactions") { + test("handle errors when signing transactions") { val bitcoinClient = new BasicBitcoinJsonRPCClient( user = config.getString("bitcoind.rpcuser"), password = config.getString("bitcoind.rpcpassword"), @@ -111,20 +111,27 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val sender = TestProbe() - // create and fund a transaction + // create a transaction that spends UTXOs that don't exist wallet.getFinalAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] - val unsignedTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0) - wallet.fundTransaction(unsignedTx, false, 1500).pipeTo(sender.ref) - val FundTransactionResponse(fundedTx, _, _) = sender.expectMsgType[FundTransactionResponse] - - // change the index of the UTXO that it spends - val fundedTx1 = fundedTx.copy(txIn = fundedTx.txIn.updated(0, fundedTx.txIn(0).copy(outPoint = fundedTx.txIn(0).outPoint.copy(index = fundedTx.txIn(0).outPoint.index + 1)))) - - // signing it should fail, and the error message should contain the txid of the UTXO that could not be used - wallet.signTransaction(fundedTx1).pipeTo(sender.ref) + val unknownTxids = Seq( + BinaryData("01" *32), + BinaryData("02" *32), + BinaryData("03" *32) + ) + val unsignedTx = Transaction(version = 2, + txIn = Seq( + TxIn(OutPoint(unknownTxids(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), + TxIn(OutPoint(unknownTxids(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), + TxIn(OutPoint(unknownTxids(2), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) + ), + txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, + lockTime = 0) + + // signing it should fail, and the error message should contain the txids of the UTXOs that could not be used + wallet.signTransaction(unsignedTx).pipeTo(sender.ref) val Failure(JsonRPCError(error)) = sender.expectMsgType[Failure] - assert(error.message.contains(fundedTx1.txIn(0).outPoint.txid.toString())) + unknownTxids.foreach(id => assert(error.message.contains(id.toString()))) } test("create/commit/rollback funding txes") { From a752d65c195a9d805c5b1520050f79d187d9d51d Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 6 Mar 2019 20:17:49 +0100 Subject: [PATCH 07/75] BitcoinCoreWalletSpec: fix formatting issue --- .../blockchain/bitcoind/BitcoinCoreWalletSpec.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index f391a17eae..01869a6a7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -115,9 +115,9 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe wallet.getFinalAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] val unknownTxids = Seq( - BinaryData("01" *32), - BinaryData("02" *32), - BinaryData("03" *32) + BinaryData("01" * 32), + BinaryData("02" * 32), + BinaryData("03" * 32) ) val unsignedTx = Transaction(version = 2, txIn = Seq( @@ -276,4 +276,4 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.expectMsg(true) } -} \ No newline at end of file +} From e148fce242f95e978f81e4105ff6e0564b3e9bfd Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 11 Mar 2019 17:32:25 +0100 Subject: [PATCH 08/75] [WIP] Migrate to new API service --- .../fr/acinq/eclair/api/JsonSerializers.scala | 4 + .../fr/acinq/eclair/api/Marshallers.scala | 46 +++++++ .../fr/acinq/eclair/api/NewService.scala | 116 ++++++++++++++++++ .../scala/fr/acinq/eclair/api/Service.scala | 1 - 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index ce3c12881a..494ae0a500 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -144,3 +144,7 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format = JField("minFinalCltvExpiry", if (p.minFinalCltvExpiry.isDefined) JLong(p.minFinalCltvExpiry.get) else JNull) :: Nil) })) + +object PublicKeySerializer extends CustomSerializer[PublicKey](format => ({ null },{ + case pk: PublicKey => JString(pk.toString()) +})) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala new file mode 100644 index 0000000000..76de7e8c2e --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala @@ -0,0 +1,46 @@ +package fr.acinq.eclair.api + +import java.net.InetAddress + +import akka.http.scaladsl.server.{PathMatcher, PathMatcher1, _} +import akka.http.scaladsl.unmarshalling.Unmarshaller +import fr.acinq.bitcoin.BinaryData +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.wire.NodeAddress +import grizzled.slf4j.Logging + +import scala.util.{Failure, Success, Try} + +object Marshallers extends Directives with Logging { + + val hexRegex = "[0-9a-fA-F]{2}".r + + implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey => + Try { + PublicKey(rawPubKey) + } match { + case Success(key) => key + case Failure(exception) => throw exception + } + } + + implicit val inetAddressUnmarshaller: Unmarshaller[String, NodeAddress] = Unmarshaller.strict { rawAddress => + val Array(host: String, port: String) = rawAddress.split(":") + NodeAddress.fromParts(host, port.toInt) match { + case Success(address) => address + case Failure(thr) => throw thr + } + } + + implicit val binaryDataUnmarshaller: Unmarshaller[String, BinaryData] = Unmarshaller.strict { hex => + BinaryData(hex) + } + + implicit val sha256HashUnmarshaller: Unmarshaller[String, BinaryData] = binaryDataUnmarshaller.map { bin => + bin.size match { + case 33 => bin + case _ => throw new IllegalArgumentException(s"$bin is not a valid SHA256 hash") + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala new file mode 100644 index 0000000000..396c979114 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -0,0 +1,116 @@ +package fr.acinq.eclair.api + +import akka.util.Timeout +import akka.pattern._ +import akka.http.scaladsl.server._ +import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} +import fr.acinq.eclair.{Kit, ShortChannelId} +import fr.acinq.eclair.io.{NodeURI, Peer} +import org.json4s.jackson +import Marshallers._ +import fr.acinq.eclair.channel.{CMD_CLOSE, Register} +import fr.acinq.eclair.wire.NodeAddress + +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration._ + +trait NewService extends Directives { + + implicit val ec: ExecutionContext + implicit val serialization = jackson.Serialization + implicit val formats = org.json4s.DefaultFormats + + new BinaryDataSerializer + + new UInt64Serializer + + new MilliSatoshiSerializer + + new ShortChannelIdSerializer + + new StateSerializer + + new ShaChainSerializer + + new PublicKeySerializer + + new PrivateKeySerializer + + new ScalarSerializer + + new PointSerializer + + new TransactionSerializer + + new TransactionWithInputInfoSerializer + + new InetSocketAddressSerializer + + new OutPointSerializer + + new OutPointKeySerializer + + new InputInfoSerializer + + new ColorSerializer + + new RouteResponseSerializer + + new ThrowableSerializer + + new FailureMessageSerializer + + new NodeAddressSerializer + + new DirectionSerializer + + new PaymentRequestSerializer + + PublicKeySerializer + + implicit val timeout = Timeout(60 seconds) + implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True + + def appKit: Kit + + val motherRoute: Route = pathSingleSlash { + get { + connectRoute ~ + openRoute ~ + closeRoute + } + } + + val connectRoute = path("connect") { + parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => + complete(connect(s"$nodeId@$addr")) + } ~ parameters("uri") { uri => + complete(connect(uri)) + } + } + + val openRoute = path("open") { + parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } + + val closeRoute = path("close") { + parameters("channelId".as[BinaryData](sha256HashUnmarshaller), "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId: BinaryData, scriptPubKey_opt: Option[BinaryData]) => + complete(close(channelId.toString(), scriptPubKey_opt)) + } + } + + + def connect(uri: String) : Future[String] = { + (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] + } + + def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { + (appKit.switchboard ? Peer.OpenChannel( + remoteNodeId = nodeId, + fundingSatoshis = Satoshi(fundingSatoshis), + pushMsat = pushMsat.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), + fundingTxFeeratePerKw_opt = fundingFeerateSatByte, + channelFlags = flags.map(_.toByte))).mapTo[String] + } + + def close(channelId: String, scriptPubKey: Option[BinaryData]) = { + sendToChannel(channelId, CMD_CLOSE(scriptPubKey)).mapTo[String] + } + + /** + * Sends a request to a channel and expects a response + * + * @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) + * @param request + * @return + */ + def sendToChannel(channelIdentifier: String, request: Any): Future[Any] = + for { + fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request)) + .recoverWith { case _ => Future(Register.Forward(BinaryData(channelIdentifier), request)) } + .recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) } + res <- appKit.register ? fwdReq + } yield res + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index af8aa89276..1c4084079e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -46,7 +46,6 @@ import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw} import grizzled.slf4j.Logging import org.json4s.JsonAST.{JBool, JInt, JString} import org.json4s.{JValue, jackson} - import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} From 6bad025be3890cf14b9fc2bfb4c4b954be4cbdd9 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 11 Mar 2019 18:29:14 +0100 Subject: [PATCH 09/75] [WIP] wire new service to api socket --- eclair-core/pom.xml | 5 ++ .../main/scala/fr/acinq/eclair/Setup.scala | 20 +++--- .../fr/acinq/eclair/api/JsonSerializers.scala | 4 -- .../fr/acinq/eclair/api/NewService.scala | 65 ++++++++++--------- 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 679f4a5915..8eae92a97f 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -132,6 +132,11 @@ akka-http-core_${scala.version.short} ${akka.http.version} + + + + + com.softwaremill.sttp diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index a9ea3d376c..071125e684 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -32,7 +32,7 @@ import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} -import fr.acinq.eclair.api.{GetInfoResponse, Service} +import fr.acinq.eclair.api.{GetInfoResponse, NewService, Service} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher} @@ -264,15 +264,15 @@ class Setup(datadir: File, _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") implicit val materializer = ActorMaterializer() - val api = new Service { + val api = new NewService { - override def scheduler = system.scheduler - - override val password = { - val p = config.getString("api.password") - if (p.isEmpty) throw EmptyAPIPasswordException else p - } +// override def scheduler = system.scheduler +// override val password = { +// val p = config.getString("api.password") +// if (p.isEmpty) throw EmptyAPIPasswordException else p +// } +// override def getInfoResponse: Future[GetInfoResponse] = Future.successful( GetInfoResponse(nodeId = nodeParams.nodeId, alias = nodeParams.alias, @@ -283,9 +283,9 @@ class Setup(datadir: File, override def appKit: Kit = kit - override val socketHandler = makeSocketHandler(system)(materializer) +// override val socketHandler = makeSocketHandler(system)(materializer) } - val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover { + val httpBound = Http().bindAndHandle(api.motherRoute, config.getString("api.binding-ip"), config.getInt("api.port")).recover { case _: BindFailedException => throw TCPBindException(config.getInt("api.port")) } val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port")))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 494ae0a500..ce3c12881a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -144,7 +144,3 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format = JField("minFinalCltvExpiry", if (p.minFinalCltvExpiry.isDefined) JLong(p.minFinalCltvExpiry.get) else JNull) :: Nil) })) - -object PublicKeySerializer extends CustomSerializer[PublicKey](format => ({ null },{ - case pk: PublicKey => JString(pk.toString()) -})) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 396c979114..6f4c16e6b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -3,22 +3,27 @@ package fr.acinq.eclair.api import akka.util.Timeout import akka.pattern._ import akka.http.scaladsl.server._ -import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty +import de.heikoseeberger.akkahttpjson4s.Json4sSupport.{ShouldWritePretty, marshaller, unmarshaller} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} import org.json4s.jackson import Marshallers._ +import de.heikoseeberger.akkahttpjson4s.Json4sSupport import fr.acinq.eclair.channel.{CMD_CLOSE, Register} import fr.acinq.eclair.wire.NodeAddress - import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait NewService extends Directives { +trait NewService { + + def appKit: Kit + + def getInfoResponse: Future[GetInfoResponse] + + implicit val ec = appKit.system.dispatcher - implicit val ec: ExecutionContext implicit val serialization = jackson.Serialization implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + @@ -43,59 +48,61 @@ trait NewService extends Directives { new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer + - new PaymentRequestSerializer + - PublicKeySerializer + new PaymentRequestSerializer implicit val timeout = Timeout(60 seconds) implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True + import Json4sSupport.{marshaller, unmarshaller} - def appKit: Kit - - val motherRoute: Route = pathSingleSlash { - get { - connectRoute ~ - openRoute ~ - closeRoute - } - } - - val connectRoute = path("connect") { + val connectRoute: Route = path("connect") { parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => - complete(connect(s"$nodeId@$addr")) + connect(s"$nodeId@$addr") } ~ parameters("uri") { uri => - complete(connect(uri)) + connect(uri) } } - val openRoute = path("open") { + val openRoute: Route = path("open") { parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) } } - val closeRoute = path("close") { + val closeRoute: Route = path("close") { parameters("channelId".as[BinaryData](sha256HashUnmarshaller), "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId: BinaryData, scriptPubKey_opt: Option[BinaryData]) => - complete(close(channelId.toString(), scriptPubKey_opt)) + close(channelId.toString(), scriptPubKey_opt) } } + val getInfoRoute: Route = path("getinfo") { + complete(getInfoResponse) + } + + val motherRoute: Route = { + get { + connectRoute ~ + openRoute ~ + closeRoute ~ + getInfoRoute + } + } - def connect(uri: String) : Future[String] = { - (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] + def connect(uri: String) : Route = { + complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) } - def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { - (appKit.switchboard ? Peer.OpenChannel( + def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Route = { + complete((appKit.switchboard ? Peer.OpenChannel( remoteNodeId = nodeId, fundingSatoshis = Satoshi(fundingSatoshis), pushMsat = pushMsat.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), fundingTxFeeratePerKw_opt = fundingFeerateSatByte, - channelFlags = flags.map(_.toByte))).mapTo[String] + channelFlags = flags.map(_.toByte))).mapTo[String]) } def close(channelId: String, scriptPubKey: Option[BinaryData]) = { - sendToChannel(channelId, CMD_CLOSE(scriptPubKey)).mapTo[String] + complete(sendToChannel(channelId, CMD_CLOSE(scriptPubKey)).mapTo[String]) } /** From 134ceeb65b7ef28aa70305e900086d547bec7243 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 11 Mar 2019 18:52:29 +0100 Subject: [PATCH 10/75] Remove swakka --- eclair-core/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 8eae92a97f..679f4a5915 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -132,11 +132,6 @@ akka-http-core_${scala.version.short} ${akka.http.version} - - - - - com.softwaremill.sttp From 2635e1dd0a47b0a930c1fbad083b9bc43cea12a0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 14:36:42 +0100 Subject: [PATCH 11/75] [WIP] port to new service more 'calls' --- .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../fr/acinq/eclair/api/JsonSerializers.scala | 33 +++- .../fr/acinq/eclair/api/NewService.scala | 180 +++++++++++------- 3 files changed, 146 insertions(+), 69 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 071125e684..6238a2edcc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -285,7 +285,7 @@ class Setup(datadir: File, // override val socketHandler = makeSocketHandler(system)(materializer) } - val httpBound = Http().bindAndHandle(api.motherRoute, config.getString("api.binding-ip"), config.getInt("api.port")).recover { + val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover { case _: BindFailedException => throw TCPBindException(config.getInt("api.port")) } val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port")))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index ce3c12881a..454d13077a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInpu import fr.acinq.eclair.wire._ import fr.acinq.eclair.{ShortChannelId, UInt64} import org.json4s.JsonAST._ -import org.json4s.{CustomKeySerializer, CustomSerializer} +import org.json4s.{CustomKeySerializer, CustomSerializer, jackson} /** * JSON Serializers. @@ -144,3 +144,34 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format = JField("minFinalCltvExpiry", if (p.minFinalCltvExpiry.isDefined) JLong(p.minFinalCltvExpiry.get) else JNull) :: Nil) })) + +trait WithJsonSerializers { + + implicit val serialization = jackson.Serialization + + implicit val formats = org.json4s.DefaultFormats + + new BinaryDataSerializer + + new UInt64Serializer + + new MilliSatoshiSerializer + + new ShortChannelIdSerializer + + new StateSerializer + + new ShaChainSerializer + + new PublicKeySerializer + + new PrivateKeySerializer + + new ScalarSerializer + + new PointSerializer + + new TransactionSerializer + + new TransactionWithInputInfoSerializer + + new InetSocketAddressSerializer + + new OutPointSerializer + + new OutPointKeySerializer + + new InputInfoSerializer + + new ColorSerializer + + new RouteResponseSerializer + + new ThrowableSerializer + + new FailureMessageSerializer + + new NodeAddressSerializer + + new DirectionSerializer + + new PaymentRequestSerializer + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 6f4c16e6b9..43d76c3fad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -8,101 +8,112 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} -import org.json4s.jackson import Marshallers._ -import de.heikoseeberger.akkahttpjson4s.Json4sSupport -import fr.acinq.eclair.channel.{CMD_CLOSE, Register} +import akka.actor.ActorRef +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.wire.NodeAddress import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait NewService { +trait NewService extends WithJsonSerializers { def appKit: Kit def getInfoResponse: Future[GetInfoResponse] implicit val ec = appKit.system.dispatcher - - implicit val serialization = jackson.Serialization - implicit val formats = org.json4s.DefaultFormats + - new BinaryDataSerializer + - new UInt64Serializer + - new MilliSatoshiSerializer + - new ShortChannelIdSerializer + - new StateSerializer + - new ShaChainSerializer + - new PublicKeySerializer + - new PrivateKeySerializer + - new ScalarSerializer + - new PointSerializer + - new TransactionSerializer + - new TransactionWithInputInfoSerializer + - new InetSocketAddressSerializer + - new OutPointSerializer + - new OutPointKeySerializer + - new InputInfoSerializer + - new ColorSerializer + - new RouteResponseSerializer + - new ThrowableSerializer + - new FailureMessageSerializer + - new NodeAddressSerializer + - new DirectionSerializer + - new PaymentRequestSerializer - implicit val timeout = Timeout(60 seconds) implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True - import Json4sSupport.{marshaller, unmarshaller} - val connectRoute: Route = path("connect") { - parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => - connect(s"$nodeId@$addr") - } ~ parameters("uri") { uri => - connect(uri) - } - } + val channelIdNamedParameter = "channelId".as[PublicKey] - val openRoute: Route = path("open") { - parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) + val route: Route = { + get { + path("getinfo") { + complete(getInfoResponse) + } ~ + path("help") { + complete(help.mkString) + } + path("connect") { + parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => + complete(connect(s"$nodeId@$addr")) + } ~ parameters("uri") { uri => + complete(connect(uri)) + } + } ~ + path("open") { + parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(close(channelId.toString(), scriptPubKey_opt)) + } + } ~ + path("forceclose") { + parameters(channelIdNamedParameter) { channelId => + complete(forceClose(channelId.toString)) + } + } ~ + path("updaterelayfee") { + parameters(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(peersInfo()) + } ~ + path("channels") { + parameters(channelIdNamedParameter.?) { channelId_opt => + complete(channels(channelId_opt)) + } + } } } - val closeRoute: Route = path("close") { - parameters("channelId".as[BinaryData](sha256HashUnmarshaller), "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId: BinaryData, scriptPubKey_opt: Option[BinaryData]) => - close(channelId.toString(), scriptPubKey_opt) - } + def connect(uri: String): Future[String] = { + (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] } - val getInfoRoute: Route = path("getinfo") { - complete(getInfoResponse) + def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { + (appKit.switchboard ? Peer.OpenChannel( + remoteNodeId = nodeId, + fundingSatoshis = Satoshi(fundingSatoshis), + pushMsat = pushMsat.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), + fundingTxFeeratePerKw_opt = fundingFeerateSatByte, + channelFlags = flags.map(_.toByte))).mapTo[String] } - val motherRoute: Route = { - get { - connectRoute ~ - openRoute ~ - closeRoute ~ - getInfoRoute - } + def close(channelId: String, scriptPubKey: Option[BinaryData]): Future[String] = { + sendToChannel(channelId, CMD_CLOSE(scriptPubKey)).mapTo[String] } - def connect(uri: String) : Route = { - complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) + def forceClose(channelId: String): Future[String] = { + sendToChannel(channelId, CMD_FORCECLOSE).mapTo[String] } - def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Route = { - complete((appKit.switchboard ? Peer.OpenChannel( - remoteNodeId = nodeId, - fundingSatoshis = Satoshi(fundingSatoshis), - pushMsat = pushMsat.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), - fundingTxFeeratePerKw_opt = fundingFeerateSatByte, - channelFlags = flags.map(_.toByte))).mapTo[String]) + def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] = { + sendToChannel(channelId, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String] } - def close(channelId: String, scriptPubKey: Option[BinaryData]) = { - complete(sendToChannel(channelId, CMD_CLOSE(scriptPubKey)).mapTo[String]) + def peersInfo(): Future[Iterable[PeerInfo]] = for { + peers <- (appKit.switchboard ? 'peers).mapTo[Iterable[ActorRef]] + peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) + } yield peerinfos + + def channels(channelIdFilter: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = channelIdFilter match { + case Some(pk) => for { + channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys) + channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + } yield channels + case None => for { + channels_id <- (appKit.register ? 'channels).mapTo[Map[BinaryData, ActorRef]].map(_.keys) + channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + } yield channels } /** @@ -120,4 +131,39 @@ trait NewService { res <- appKit.register ? fwdReq } yield res + def help = List( + "connect (uri): open a secure connection to a lightning node", + "connect (nodeId, host, port): open a secure connection to a lightning node", + "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", + "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", + "peers: list existing local peers", + "channels: list existing local channels", + "channels (nodeId): list existing local channels to a particular nodeId", + "channel (channelId): retrieve detailed information about a given channel", + "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", + "allnodes: list all known nodes", + "allchannels: list all known channels", + "allupdates: list all channels updates", + "allupdates (nodeId): list all channels updates for this nodeId", + "receive (amountMsat, description): generate a payment request for a given amount", + "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", + "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", + "findroute (paymentRequest): returns nodes and channels of the route if there is any", + "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", + "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", + "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", + "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", + "close (channelId): close a channel", + "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", + "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", + "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", + "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", + "audit: list all send/received/relayed payments", + "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", + "networkfees: list all network fees paid to the miners, by transaction", + "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", + "getinfo: returns info about the blockchain and this node", + "help: display this message") + } \ No newline at end of file From dd909bf5c7a4153466e60f30fb5cd1115d657eff Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 15:00:29 +0100 Subject: [PATCH 12/75] [WIP] port to new service allupdates calls --- .../fr/acinq/eclair/api/NewService.scala | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 43d76c3fad..77e16d2173 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -12,7 +12,9 @@ import Marshallers._ import akka.actor.ActorRef import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import fr.acinq.eclair.wire.NodeAddress +import fr.acinq.eclair.router.ChannelDesc +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -26,16 +28,13 @@ trait NewService extends WithJsonSerializers { implicit val timeout = Timeout(60 seconds) implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True - val channelIdNamedParameter = "channelId".as[PublicKey] + // a named and typed URL parameter used across several routes, 32-bytes hex-encoded + val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) val route: Route = { get { - path("getinfo") { - complete(getInfoResponse) - } ~ - path("help") { - complete(help.mkString) - } + path("getinfo") { complete(getInfoResponse) } ~ + path("help") { complete(help.mkString) } ~ path("connect") { parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => complete(connect(s"$nodeId@$addr")) @@ -51,7 +50,7 @@ trait NewService extends WithJsonSerializers { } ~ path("close") { parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(channelId.toString(), scriptPubKey_opt)) + complete(close(channelId, scriptPubKey_opt)) } } ~ path("forceclose") { @@ -68,8 +67,20 @@ trait NewService extends WithJsonSerializers { complete(peersInfo()) } ~ path("channels") { - parameters(channelIdNamedParameter.?) { channelId_opt => - complete(channels(channelId_opt)) + parameters("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + parameters(channelIdNamedParameter) { channelId => + complete(channelInfo(channelId)) + } + } ~ + path("allnodes") { complete(allnodes()) } ~ + path("allchannels") { complete(allchannels()) } ~ + path("allupdates") { + parameters("nodeId".as[PublicKey].?) { nodeId_opt => + complete(allupdates(nodeId_opt)) } } } @@ -88,8 +99,8 @@ trait NewService extends WithJsonSerializers { channelFlags = flags.map(_.toByte))).mapTo[String] } - def close(channelId: String, scriptPubKey: Option[BinaryData]): Future[String] = { - sendToChannel(channelId, CMD_CLOSE(scriptPubKey)).mapTo[String] + def close(channelId: BinaryData, scriptPubKey: Option[BinaryData]): Future[String] = { + sendToChannel(channelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String] } def forceClose(channelId: String): Future[String] = { @@ -105,7 +116,7 @@ trait NewService extends WithJsonSerializers { peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) } yield peerinfos - def channels(channelIdFilter: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = channelIdFilter match { + def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = toRemoteNode match { case Some(pk) => for { channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys) channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) @@ -116,6 +127,21 @@ trait NewService extends WithJsonSerializers { } yield channels } + def channelInfo(channelId: BinaryData): Future[RES_GETINFO] = { + sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO] + } + + def allnodes(): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]] + + def allchannels(): Future[Iterable[ChannelDesc]] = { + (appKit.router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))) + } + + def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] = nodeId match { + case None => (appKit.router ? 'updates).mapTo[Iterable[ChannelUpdate]] + case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) + } + /** * Sends a request to a channel and expects a response * From 848563b87a3f22e4a8ae20f7431c23536d7514b8 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 16:11:30 +0100 Subject: [PATCH 13/75] [WIP] port to new service receive/parseinvoice/findroute/send --- .../fr/acinq/eclair/api/Marshallers.scala | 10 ++++ .../fr/acinq/eclair/api/NewService.scala | 53 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala index 76de7e8c2e..3c880a5f70 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala @@ -6,6 +6,7 @@ import akka.http.scaladsl.server.{PathMatcher, PathMatcher1, _} import akka.http.scaladsl.unmarshalling.Unmarshaller import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.wire.NodeAddress import grizzled.slf4j.Logging @@ -43,4 +44,13 @@ object Marshallers extends Directives with Logging { } } + implicit val bolt11Unmarshaller: Unmarshaller[String, PaymentRequest] = Unmarshaller.strict { rawRequest => + Try { + PaymentRequest.read(rawRequest) + } match { + case Success(req) => req + case Failure(exception) => throw exception + } + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 77e16d2173..536e4edaa7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -12,7 +12,9 @@ import Marshallers._ import akka.actor.ActorRef import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import fr.acinq.eclair.router.ChannelDesc +import fr.acinq.eclair.payment.PaymentLifecycle._ +import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} +import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import scala.concurrent.{ExecutionContext, Future} @@ -82,6 +84,26 @@ trait NewService extends WithJsonSerializers { parameters("nodeId".as[PublicKey].?) { nodeId_opt => complete(allupdates(nodeId_opt)) } + } ~ + path("receive") { + parameters("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + parameters("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findroute") { + parameters("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => + complete(findRoute(nodeId, amount, invoice)) + } + } ~ + path("send") { + parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + complete(send(nodeId, amountMsat, paymentHash, invoice)) + } } } } @@ -142,6 +164,35 @@ trait NewService extends WithJsonSerializers { case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } + def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = { + (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire)).mapTo[String] + } + + def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Future[RouteResponse] = (nodeId_opt, amount_opt, invoice_opt) match { + case (None, None, Some(invoice@PaymentRequest(_, Some(amountMsat), _, targetNodeId, _, _))) => + (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat.toLong, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse] + case (None, Some(amountMsat), Some(invoice)) => + (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse] + case (Some(nodeId), Some(amountMsat), None) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse] + case _ => throw new IllegalArgumentException("Wrong params for findRoute, call 'help' to know more about it") + } + + def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[PaymentResult] = { + val (targetNodeId, paymentHash, amountMsat) = (nodeId_opt, amount_opt, paymentHash_opt, invoice_opt) match { + case (Some(nodeId), Some(amountMsat), Some(paymentHash), None) => (nodeId, paymentHash, amountMsat) + case (None, None, None, Some(invoice@PaymentRequest(_, Some(amountMsat), _, targetNodeId, _, _))) => (targetNodeId, invoice.paymentHash, amountMsat.toLong) + case (None, Some(amountMsat), None, Some(invoice@PaymentRequest(_, Some(_), _, targetNodeId, _, _))) => (targetNodeId, invoice.paymentHash, amountMsat) // invoice amount is overridden + case _ => ??? + } + + val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry + + (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { + case s: PaymentSucceeded => s + case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) + } + } + /** * Sends a request to a channel and expects a response * From 8aebef538e5d97f8302cad87cab654cf58a9bae7 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 17:09:07 +0100 Subject: [PATCH 14/75] [WIP] port to new service checkpayment/audit/networkfees/channelStats --- .../fr/acinq/eclair/api/NewService.scala | 60 +++++++++++++++++-- ...hallers.scala => UrlParamExtractors.scala} | 12 +--- 2 files changed, 57 insertions(+), 15 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/api/{Marshallers.scala => UrlParamExtractors.scala} (86%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 536e4edaa7..6b2fddc8e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -8,19 +8,19 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} -import Marshallers._ +import UrlParamExtractors._ import akka.actor.ActorRef import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.{NetworkFee, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} - import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait NewService extends WithJsonSerializers { +trait NewService extends Directives with WithJsonSerializers { def appKit: Kit @@ -104,6 +104,24 @@ trait NewService extends WithJsonSerializers { parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => complete(send(nodeId, amountMsat, paymentHash, invoice)) } + } ~ + path("checkpayment") { + parameters("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + complete(checkpayment(paymentHash, invoice)) + } + } ~ + path("audit") { + parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(audit(from, to)) + } + } ~ + path("networkfees") { + parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(channelStats()) } } } @@ -179,9 +197,9 @@ trait NewService extends WithJsonSerializers { def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[PaymentResult] = { val (targetNodeId, paymentHash, amountMsat) = (nodeId_opt, amount_opt, paymentHash_opt, invoice_opt) match { - case (Some(nodeId), Some(amountMsat), Some(paymentHash), None) => (nodeId, paymentHash, amountMsat) - case (None, None, None, Some(invoice@PaymentRequest(_, Some(amountMsat), _, targetNodeId, _, _))) => (targetNodeId, invoice.paymentHash, amountMsat.toLong) - case (None, Some(amountMsat), None, Some(invoice@PaymentRequest(_, Some(_), _, targetNodeId, _, _))) => (targetNodeId, invoice.paymentHash, amountMsat) // invoice amount is overridden + case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) + case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) + case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden case _ => ??? } @@ -193,6 +211,36 @@ trait NewService extends WithJsonSerializers { } } + def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[Boolean] = (paymentHash_opt, invoice_opt) match { + case (Some(ph), None) => (appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean] + case (None, Some(invoice)) => (appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean] + case _ => ??? + } + + def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { + val (from, to) = (from_opt, to_opt) match { + case (Some(f), Some(t)) => (f, t) + case _ => (0L, Long.MaxValue) + } + + Future(AuditResponse( + sent = appKit.nodeParams.auditDb.listSent(from, to), + received = appKit.nodeParams.auditDb.listReceived(from, to), + relayed = appKit.nodeParams.auditDb.listRelayed(from, to) + )) + } + + def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] = { + val (from, to) = (from_opt, to_opt) match { + case (Some(f), Some(t)) => (f, t) + case _ => (0L, Long.MaxValue) + } + + Future(appKit.nodeParams.auditDb.listNetworkFees(from, to)) + } + + def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.auditDb.stats) + /** * Sends a request to a channel and expects a response * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/UrlParamExtractors.scala similarity index 86% rename from eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/api/UrlParamExtractors.scala index 3c880a5f70..3c605de69e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Marshallers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/UrlParamExtractors.scala @@ -1,20 +1,13 @@ package fr.acinq.eclair.api -import java.net.InetAddress - -import akka.http.scaladsl.server.{PathMatcher, PathMatcher1, _} import akka.http.scaladsl.unmarshalling.Unmarshaller import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.wire.NodeAddress -import grizzled.slf4j.Logging - import scala.util.{Failure, Success, Try} -object Marshallers extends Directives with Logging { - - val hexRegex = "[0-9a-fA-F]{2}".r +object UrlParamExtractors { implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey => Try { @@ -25,6 +18,7 @@ object Marshallers extends Directives with Logging { } } + // assumes IPv4 like XXX.YYY.ZZZ.EEE:1234 implicit val inetAddressUnmarshaller: Unmarshaller[String, NodeAddress] = Unmarshaller.strict { rawAddress => val Array(host: String, port: String) = rawAddress.split(":") NodeAddress.fromParts(host, port.toInt) match { @@ -39,7 +33,7 @@ object Marshallers extends Directives with Logging { implicit val sha256HashUnmarshaller: Unmarshaller[String, BinaryData] = binaryDataUnmarshaller.map { bin => bin.size match { - case 33 => bin + case 32 => bin case _ => throw new IllegalArgumentException(s"$bin is not a valid SHA256 hash") } } From 1e8c2339e29b71e7a8c80cf355e292808f0c1611 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 17:44:15 +0100 Subject: [PATCH 15/75] [WIP] port to new service the websocket --- .../fr/acinq/eclair/api/NewService.scala | 226 +++++++++++------- 1 file changed, 133 insertions(+), 93 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 6b2fddc8e7..81aa90cfd2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -9,24 +9,33 @@ import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} import UrlParamExtractors._ -import akka.actor.ActorRef +import akka.NotUsed +import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.ws.{Message, TextMessage} +import akka.http.scaladsl.server.Directives.{complete, extractRequest} +import akka.stream.{ActorMaterializer, OverflowStrategy} +import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{NetworkFee, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.payment.PaymentLifecycle._ -import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} +import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} +import grizzled.slf4j.Logging + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait NewService extends Directives with WithJsonSerializers { +trait NewService extends Directives with WithJsonSerializers with Logging { def appKit: Kit def getInfoResponse: Future[GetInfoResponse] implicit val ec = appKit.system.dispatcher + implicit val mat: ActorMaterializer implicit val timeout = Timeout(60 seconds) implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True @@ -34,94 +43,99 @@ trait NewService extends Directives with WithJsonSerializers { val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) val route: Route = { - get { - path("getinfo") { complete(getInfoResponse) } ~ - path("help") { complete(help.mkString) } ~ - path("connect") { - parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => - complete(connect(s"$nodeId@$addr")) - } ~ parameters("uri") { uri => - complete(connect(uri)) - } - } ~ - path("open") { - parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(channelId, scriptPubKey_opt)) - } - } ~ - path("forceclose") { - parameters(channelIdNamedParameter) { channelId => - complete(forceClose(channelId.toString)) - } - } ~ - path("updaterelayfee") { - parameters(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(peersInfo()) - } ~ - path("channels") { - parameters("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - parameters(channelIdNamedParameter) { channelId => - complete(channelInfo(channelId)) - } - } ~ - path("allnodes") { complete(allnodes()) } ~ - path("allchannels") { complete(allchannels()) } ~ - path("allupdates") { - parameters("nodeId".as[PublicKey].?) { nodeId_opt => - complete(allupdates(nodeId_opt)) - } - } ~ - path("receive") { - parameters("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - parameters("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findroute") { - parameters("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => - complete(findRoute(nodeId, amount, invoice)) - } - } ~ - path("send") { - parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => - complete(send(nodeId, amountMsat, paymentHash, invoice)) + handleExceptions(apiExceptionHandler){ + get { + path("getinfo") { complete(getInfoResponse) } ~ + path("help") { complete(help.mkString) } ~ + path("connect") { + parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => + complete(connect(s"$nodeId@$addr")) + } ~ parameters("uri") { uri => + complete(connect(uri)) + } + } ~ + path("open") { + parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(close(channelId, scriptPubKey_opt)) + } + } ~ + path("forceclose") { + parameters(channelIdNamedParameter) { channelId => + complete(forceClose(channelId.toString)) + } + } ~ + path("updaterelayfee") { + parameters(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(peersInfo()) + } ~ + path("channels") { + parameters("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + parameters(channelIdNamedParameter) { channelId => + complete(channelInfo(channelId)) + } + } ~ + path("allnodes") { complete(allnodes()) } ~ + path("allchannels") { complete(allchannels()) } ~ + path("allupdates") { + parameters("nodeId".as[PublicKey].?) { nodeId_opt => + complete(allupdates(nodeId_opt)) + } + } ~ + path("receive") { + parameters("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + parameters("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findroute") { + parameters("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => + complete(findRoute(nodeId, amount, invoice)) + } + } ~ + path("send") { + parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + complete(send(nodeId, amountMsat, paymentHash, invoice)) + } + } ~ + path("checkpayment") { + parameters("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + complete(checkpayment(paymentHash, invoice)) + } + } ~ + path("audit") { + parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(audit(from, to)) + } + } ~ + path("networkfees") { + parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) } - } ~ - path("checkpayment") { - parameters("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => - complete(checkpayment(paymentHash, invoice)) - } - } ~ - path("audit") { - parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(audit(from, to)) - } - } ~ - path("networkfees") { - parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(networkFees(from, to)) - } - } ~ - path("channelstats") { - complete(channelStats()) } } } @@ -192,7 +206,7 @@ trait NewService extends Directives with WithJsonSerializers { case (None, Some(amountMsat), Some(invoice)) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse] case (Some(nodeId), Some(amountMsat), None) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse] - case _ => throw new IllegalArgumentException("Wrong params for findRoute, call 'help' to know more about it") + case _ => throw ApiError("findroute", "Wrong params list, call 'help' to know more about it") } def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[PaymentResult] = { @@ -200,7 +214,7 @@ trait NewService extends Directives with WithJsonSerializers { case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden - case _ => ??? + case _ => throw ApiError("send", "Wrong params list, call 'help' to know more about it") } val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry @@ -214,7 +228,7 @@ trait NewService extends Directives with WithJsonSerializers { def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[Boolean] = (paymentHash_opt, invoice_opt) match { case (Some(ph), None) => (appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean] case (None, Some(invoice)) => (appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean] - case _ => ??? + case _ => throw ApiError("checkpayment", "Wrong params list, call 'help' to know more about it") } def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { @@ -291,4 +305,30 @@ trait NewService extends Directives with WithJsonSerializers { "getinfo: returns info about the blockchain and this node", "help: display this message") + lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { + + // create a flow transforming a queue of string -> string + val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() + + // register an actor that feeds the queue when a payment is received + appKit.system.actorOf(Props(new Actor { + override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) + def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) } + })) + + Flow[Message] + .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client + .merge(flowOutput) // Stream the data we want to the client + .map(TextMessage.apply) + } + + val apiExceptionHandler = ExceptionHandler { + case e: ApiError => complete(StatusCodes.BadRequest, e.msg) + case t: Throwable => + logger.error(s"API call failed with cause=${t.getMessage}") + complete(StatusCodes.InternalServerError, s"Error: $t") + } + + case class ApiError(apiMethod: String, msg: String) extends RuntimeException(s"Error calling $apiMethod: $msg") + } \ No newline at end of file From 528f195c453b1efd6747ee84c284a9e27df440b6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 17:50:58 +0100 Subject: [PATCH 16/75] [WIP] port to new service the http-basic auth --- .../main/scala/fr/acinq/eclair/Setup.scala | 17 +- .../fr/acinq/eclair/api/NewService.scala | 193 +++++++++--------- 2 files changed, 109 insertions(+), 101 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 6238a2edcc..caa2958ded 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -266,13 +266,15 @@ class Setup(datadir: File, implicit val materializer = ActorMaterializer() val api = new NewService { -// override def scheduler = system.scheduler + override def appKit: Kit = kit + + override val mat = materializer + + override val password = { + val p = config.getString("api.password") + if (p.isEmpty) throw EmptyAPIPasswordException else p + } -// override val password = { -// val p = config.getString("api.password") -// if (p.isEmpty) throw EmptyAPIPasswordException else p -// } -// override def getInfoResponse: Future[GetInfoResponse] = Future.successful( GetInfoResponse(nodeId = nodeParams.nodeId, alias = nodeParams.alias, @@ -281,9 +283,6 @@ class Setup(datadir: File, blockHeight = Globals.blockCount.intValue(), publicAddresses = nodeParams.publicAddresses)) - override def appKit: Kit = kit - -// override val socketHandler = makeSocketHandler(system)(materializer) } val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover { case _: BindFailedException => throw TCPBindException(config.getInt("api.port")) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 81aa90cfd2..b1ffe824bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -13,7 +13,7 @@ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.http.scaladsl.server.Directives.{complete, extractRequest} +import akka.http.scaladsl.server.directives.Credentials import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} import fr.acinq.eclair.channel._ @@ -34,6 +34,8 @@ trait NewService extends Directives with WithJsonSerializers with Logging { def getInfoResponse: Future[GetInfoResponse] + def password: String + implicit val ec = appKit.system.dispatcher implicit val mat: ActorMaterializer implicit val timeout = Timeout(60 seconds) @@ -44,97 +46,99 @@ trait NewService extends Directives with WithJsonSerializers with Logging { val route: Route = { handleExceptions(apiExceptionHandler){ - get { - path("getinfo") { complete(getInfoResponse) } ~ - path("help") { complete(help.mkString) } ~ - path("connect") { - parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => - complete(connect(s"$nodeId@$addr")) - } ~ parameters("uri") { uri => - complete(connect(uri)) - } - } ~ - path("open") { - parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(channelId, scriptPubKey_opt)) - } - } ~ - path("forceclose") { - parameters(channelIdNamedParameter) { channelId => - complete(forceClose(channelId.toString)) - } - } ~ - path("updaterelayfee") { - parameters(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(peersInfo()) - } ~ - path("channels") { - parameters("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - parameters(channelIdNamedParameter) { channelId => - complete(channelInfo(channelId)) - } - } ~ - path("allnodes") { complete(allnodes()) } ~ - path("allchannels") { complete(allchannels()) } ~ - path("allupdates") { - parameters("nodeId".as[PublicKey].?) { nodeId_opt => - complete(allupdates(nodeId_opt)) - } - } ~ - path("receive") { - parameters("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - parameters("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findroute") { - parameters("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => - complete(findRoute(nodeId, amount, invoice)) - } - } ~ - path("send") { - parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => - complete(send(nodeId, amountMsat, paymentHash, invoice)) - } - } ~ - path("checkpayment") { - parameters("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => - complete(checkpayment(paymentHash, invoice)) - } - } ~ - path("audit") { - parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(audit(from, to)) - } - } ~ - path("networkfees") { - parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(networkFees(from, to)) - } - } ~ - path("channelstats") { - complete(channelStats()) - } ~ - path("ws") { - handleWebSocketMessages(makeSocketHandler) + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator){ _ => + get { + path("getinfo") { complete(getInfoResponse) } ~ + path("help") { complete(help.mkString) } ~ + path("connect") { + parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => + complete(connect(s"$nodeId@$addr")) + } ~ parameters("uri") { uri => + complete(connect(uri)) + } + } ~ + path("open") { + parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(close(channelId, scriptPubKey_opt)) + } + } ~ + path("forceclose") { + parameters(channelIdNamedParameter) { channelId => + complete(forceClose(channelId.toString)) + } + } ~ + path("updaterelayfee") { + parameters(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(peersInfo()) + } ~ + path("channels") { + parameters("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + parameters(channelIdNamedParameter) { channelId => + complete(channelInfo(channelId)) + } + } ~ + path("allnodes") { complete(allnodes()) } ~ + path("allchannels") { complete(allchannels()) } ~ + path("allupdates") { + parameters("nodeId".as[PublicKey].?) { nodeId_opt => + complete(allupdates(nodeId_opt)) + } + } ~ + path("receive") { + parameters("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + parameters("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findroute") { + parameters("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => + complete(findRoute(nodeId, amount, invoice)) + } + } ~ + path("send") { + parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + complete(send(nodeId, amountMsat, paymentHash, invoice)) + } + } ~ + path("checkpayment") { + parameters("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + complete(checkpayment(paymentHash, invoice)) + } + } ~ + path("audit") { + parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(audit(from, to)) + } + } ~ + path("networkfees") { + parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) + } } } } @@ -329,6 +333,11 @@ trait NewService extends Directives with WithJsonSerializers with Logging { complete(StatusCodes.InternalServerError, s"Error: $t") } + def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { + case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) + case _ => akka.pattern.after(1 second, using = appKit.system.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force + } + case class ApiError(apiMethod: String, msg: String) extends RuntimeException(s"Error calling $apiMethod: $msg") } \ No newline at end of file From 514cc356b7d112b8af0cdeaba4c0b23559399226 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 18:03:26 +0100 Subject: [PATCH 17/75] [WIP] port to new service reorg the exception handler to make it work for real! --- .../fr/acinq/eclair/api/NewService.scala | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index b1ffe824bd..e8d0fa494d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -24,7 +24,6 @@ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentReques import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import grizzled.slf4j.Logging - import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -44,6 +43,30 @@ trait NewService extends Directives with WithJsonSerializers with Logging { // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) + val apiExceptionHandler = ExceptionHandler { + case e: ApiError => complete(StatusCodes.BadRequest, e.msg) + case t: Throwable => + logger.error(s"API call failed with cause=${t.getMessage}") + complete(StatusCodes.InternalServerError, s"Error: $t") + } + + lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { + + // create a flow transforming a queue of string -> string + val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() + + // register an actor that feeds the queue when a payment is received + appKit.system.actorOf(Props(new Actor { + override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) + def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) } + })) + + Flow[Message] + .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client + .merge(flowOutput) // Stream the data we want to the client + .map(TextMessage.apply) + } + val route: Route = { handleExceptions(apiExceptionHandler){ authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator){ _ => @@ -309,30 +332,6 @@ trait NewService extends Directives with WithJsonSerializers with Logging { "getinfo: returns info about the blockchain and this node", "help: display this message") - lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { - - // create a flow transforming a queue of string -> string - val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() - - // register an actor that feeds the queue when a payment is received - appKit.system.actorOf(Props(new Actor { - override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) - def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) } - })) - - Flow[Message] - .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client - .merge(flowOutput) // Stream the data we want to the client - .map(TextMessage.apply) - } - - val apiExceptionHandler = ExceptionHandler { - case e: ApiError => complete(StatusCodes.BadRequest, e.msg) - case t: Throwable => - logger.error(s"API call failed with cause=${t.getMessage}") - complete(StatusCodes.InternalServerError, s"Error: $t") - } - def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) case _ => akka.pattern.after(1 second, using = appKit.system.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force From 36da70e1dcb60c4b9fb8fb6996ed286b2d2a3b5d Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 12 Mar 2019 18:11:31 +0100 Subject: [PATCH 18/75] Add configuration to enable the new APIs --- eclair-core/src/main/resources/reference.conf | 1 + .../main/scala/fr/acinq/eclair/Setup.scala | 26 ++++++++++++++++++- .../fr/acinq/eclair/api/MetaService.scala | 9 +++++++ .../fr/acinq/eclair/api/NewService.scala | 2 +- .../scala/fr/acinq/eclair/api/Service.scala | 2 +- 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 63863ca1a3..3c17f11351 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -13,6 +13,7 @@ eclair { binding-ip = "127.0.0.1" port = 8080 password = "" // password for basic auth, must be non empty if json-rpc api is enabled + use-new-version = false } watcher-type = "bitcoind" // other *experimental* values include "electrum" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index caa2958ded..dad2f01566 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -264,7 +264,8 @@ class Setup(datadir: File, _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") implicit val materializer = ActorMaterializer() - val api = new NewService { + val api = if(config.getBoolean("api.use-new-version")){ + new NewService { override def appKit: Kit = kit @@ -284,6 +285,29 @@ class Setup(datadir: File, publicAddresses = nodeParams.publicAddresses)) } + } else { + new Service { + + override def scheduler = system.scheduler + + override val password = { + val p = config.getString("api.password") + if (p.isEmpty) throw EmptyAPIPasswordException else p + } + + override def getInfoResponse: Future[GetInfoResponse] = Future.successful( + GetInfoResponse(nodeId = nodeParams.nodeId, + alias = nodeParams.alias, + port = config.getInt("server.port"), + chainHash = nodeParams.chainHash, + blockHeight = Globals.blockCount.intValue(), + publicAddresses = nodeParams.publicAddresses)) + + override def appKit: Kit = kit + + override val socketHandler = makeSocketHandler(system)(materializer) + } + } val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover { case _: BindFailedException => throw TCPBindException(config.getInt("api.port")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala new file mode 100644 index 0000000000..ee36d035dc --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala @@ -0,0 +1,9 @@ +package fr.acinq.eclair.api + +import akka.http.scaladsl.server.Route + +trait MetaService { + + val route: Route + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index e8d0fa494d..5429bcdc9a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -27,7 +27,7 @@ import grizzled.slf4j.Logging import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait NewService extends Directives with WithJsonSerializers with Logging { +trait NewService extends Directives with WithJsonSerializers with Logging with MetaService { def appKit: Kit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 1c4084079e..c55792097b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -66,7 +66,7 @@ final case class RpcValidationRejection(requestId: String, message: String) exte final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection // @formatter:on -trait Service extends Logging { +trait Service extends Logging with MetaService { implicit def ec: ExecutionContext = ExecutionContext.Implicits.global From 066634f3a2998ab87fb41b49091cacad1bd38ece Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 10:16:42 +0100 Subject: [PATCH 19/75] Use HTTP POST and formParams --- ...actors.scala => FormParamExtractors.scala} | 2 +- .../fr/acinq/eclair/api/NewService.scala | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/api/{UrlParamExtractors.scala => FormParamExtractors.scala} (98%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/UrlParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala similarity index 98% rename from eclair-core/src/main/scala/fr/acinq/eclair/api/UrlParamExtractors.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index 3c605de69e..d1b1c99c3d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/UrlParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -7,7 +7,7 @@ import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.wire.NodeAddress import scala.util.{Failure, Success, Try} -object UrlParamExtractors { +object FormParamExtractors { implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey => Try { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 5429bcdc9a..4124c4618d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -8,7 +8,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} -import UrlParamExtractors._ +import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.http.scaladsl.model.StatusCodes @@ -70,34 +70,34 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M val route: Route = { handleExceptions(apiExceptionHandler){ authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator){ _ => - get { + post { path("getinfo") { complete(getInfoResponse) } ~ path("help") { complete(help.mkString) } ~ path("connect") { - parameters("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => + formFields("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => complete(connect(s"$nodeId@$addr")) - } ~ parameters("uri") { uri => + } ~ formFields("uri") { uri => complete(connect(uri)) } } ~ path("open") { - parameters("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) } } ~ path("close") { - parameters(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => complete(close(channelId, scriptPubKey_opt)) } } ~ path("forceclose") { - parameters(channelIdNamedParameter) { channelId => + formFields(channelIdNamedParameter) { channelId => complete(forceClose(channelId.toString)) } } ~ path("updaterelayfee") { - parameters(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) } } ~ @@ -105,54 +105,54 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M complete(peersInfo()) } ~ path("channels") { - parameters("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => complete(channelsInfo(toRemoteNodeId_opt)) } } ~ path("channel") { - parameters(channelIdNamedParameter) { channelId => + formFields(channelIdNamedParameter) { channelId => complete(channelInfo(channelId)) } } ~ path("allnodes") { complete(allnodes()) } ~ path("allchannels") { complete(allchannels()) } ~ path("allupdates") { - parameters("nodeId".as[PublicKey].?) { nodeId_opt => + formFields("nodeId".as[PublicKey].?) { nodeId_opt => complete(allupdates(nodeId_opt)) } } ~ path("receive") { - parameters("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => complete(receive(desc, amountMsat, expire)) } } ~ path("parseinvoice") { - parameters("invoice".as[PaymentRequest]) { invoice => + formFields("invoice".as[PaymentRequest]) { invoice => complete(invoice) } } ~ path("findroute") { - parameters("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => + formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => complete(findRoute(nodeId, amount, invoice)) } } ~ path("send") { - parameters("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => complete(send(nodeId, amountMsat, paymentHash, invoice)) } } ~ path("checkpayment") { - parameters("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => complete(checkpayment(paymentHash, invoice)) } } ~ path("audit") { - parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => complete(audit(from, to)) } } ~ path("networkfees") { - parameters("from".as[Long].?, "to".as[Long].?) { (from, to) => + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => complete(networkFees(from, to)) } } ~ From 7908f5e58d8be2ea5fa0fb92374c4c277ad6b7b8 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 10:32:17 +0100 Subject: [PATCH 20/75] Use custom http headers in all API response --- .../fr/acinq/eclair/api/NewService.scala | 205 ++++++++++-------- 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 4124c4618d..74df84de7f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -11,7 +11,10 @@ import fr.acinq.eclair.io.{NodeURI, Peer} import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import akka.http.scaladsl.model.HttpMethods.POST import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} +import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} import akka.http.scaladsl.model.ws.{Message, TextMessage} import akka.http.scaladsl.server.directives.Credentials import akka.stream.{ActorMaterializer, OverflowStrategy} @@ -21,9 +24,10 @@ import fr.acinq.eclair.db.{NetworkFee, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} -import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} +import fr.acinq.eclair.router.{ChannelDesc, RouteNotFound, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import grizzled.slf4j.Logging + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -44,12 +48,19 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) val apiExceptionHandler = ExceptionHandler { - case e: ApiError => complete(StatusCodes.BadRequest, e.msg) + case e: ApiError => + e.thr.foreach(thr => logger.warn(s"caught $thr")) + complete(StatusCodes.BadRequest, e.msg) case t: Throwable => logger.error(s"API call failed with cause=${t.getMessage}") complete(StatusCodes.InternalServerError, s"Error: $t") } + val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: + `Access-Control-Allow-Methods`(POST) :: + `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil + + lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { // create a flow transforming a queue of string -> string @@ -68,100 +79,102 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M } val route: Route = { - handleExceptions(apiExceptionHandler){ - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator){ _ => - post { - path("getinfo") { complete(getInfoResponse) } ~ - path("help") { complete(help.mkString) } ~ - path("connect") { - formFields("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => - complete(connect(s"$nodeId@$addr")) - } ~ formFields("uri") { uri => - complete(connect(uri)) - } - } ~ - path("open") { - formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(channelId, scriptPubKey_opt)) - } - } ~ - path("forceclose") { - formFields(channelIdNamedParameter) { channelId => - complete(forceClose(channelId.toString)) - } - } ~ - path("updaterelayfee") { - formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(peersInfo()) - } ~ - path("channels") { - formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - formFields(channelIdNamedParameter) { channelId => - complete(channelInfo(channelId)) - } - } ~ - path("allnodes") { complete(allnodes()) } ~ - path("allchannels") { complete(allchannels()) } ~ - path("allupdates") { - formFields("nodeId".as[PublicKey].?) { nodeId_opt => - complete(allupdates(nodeId_opt)) - } - } ~ - path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - formFields("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findroute") { - formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => - complete(findRoute(nodeId, amount, invoice)) - } - } ~ - path("send") { - formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => - complete(send(nodeId, amountMsat, paymentHash, invoice)) - } - } ~ - path("checkpayment") { - formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => - complete(checkpayment(paymentHash, invoice)) - } - } ~ - path("audit") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(audit(from, to)) - } - } ~ - path("networkfees") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(networkFees(from, to)) + respondWithDefaultHeaders(customHeaders){ + handleExceptions(apiExceptionHandler){ + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator){ _ => + post { + path("getinfo") { complete(getInfoResponse) } ~ + path("help") { complete(help.mkString) } ~ + path("connect") { + formFields("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => + complete(connect(s"$nodeId@$addr")) + } ~ formFields("uri") { uri => + complete(connect(uri)) + } + } ~ + path("open") { + formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(close(channelId, scriptPubKey_opt)) + } + } ~ + path("forceclose") { + formFields(channelIdNamedParameter) { channelId => + complete(forceClose(channelId.toString)) + } + } ~ + path("updaterelayfee") { + formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(peersInfo()) + } ~ + path("channels") { + formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + formFields(channelIdNamedParameter) { channelId => + complete(channelInfo(channelId)) + } + } ~ + path("allnodes") { complete(allnodes()) } ~ + path("allchannels") { complete(allchannels()) } ~ + path("allupdates") { + formFields("nodeId".as[PublicKey].?) { nodeId_opt => + complete(allupdates(nodeId_opt)) + } + } ~ + path("receive") { + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + formFields("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findroute") { + formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => + complete(findRoute(nodeId, amount, invoice)) + } + } ~ + path("send") { + formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + complete(send(nodeId, amountMsat, paymentHash, invoice)) + } + } ~ + path("checkpayment") { + formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + complete(checkpayment(paymentHash, invoice)) + } + } ~ + path("audit") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(audit(from, to)) + } + } ~ + path("networkfees") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) } - } ~ - path("channelstats") { - complete(channelStats()) - } ~ - path("ws") { - handleWebSocketMessages(makeSocketHandler) - } + } } } } @@ -337,6 +350,6 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M case _ => akka.pattern.after(1 second, using = appKit.system.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force } - case class ApiError(apiMethod: String, msg: String) extends RuntimeException(s"Error calling $apiMethod: $msg") + case class ApiError(apiMethod: String, msg: String, thr: Option[Throwable] = None) extends RuntimeException(s"Error calling $apiMethod: $msg") } \ No newline at end of file From af9a9691bcd4baada6251a1b2db56a31bd588216 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 13:13:01 +0100 Subject: [PATCH 21/75] [WIP] fix eclair-cli to use the new APIs --- eclair-core/eclair-cli | 21 ++++++++---- .../fr/acinq/eclair/api/NewService.scala | 32 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 66e9cd3582..fac784925f 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -7,7 +7,7 @@ command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborti FULL_OUTPUT='false' URL='http://localhost:8080' -PASSWORD='' +PASSWORD='foobar' # -------------------- METHODS @@ -31,13 +31,19 @@ Full documentation at: " # Executes a JSON RPC call to a node listening on ${URL} call() { - jqexp='if .error == null then .result else .error.message end' + jqexp='.' # 'if .error == null then .result else .error.message end' # override default jq parsing expression if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi # set password if [ -z ${PASSWORD} ]; then auth="eclair-cli"; else auth="eclair-cli:"${PASSWORD}; fi - eval curl "--user ${auth} --silent --show-error -X POST -H \"Content-Type: application/json\" -d '{ \"method\": \"'${1}'\", \"params\": '${2}' }' ${URL}" | jq -r "$jqexp" + # collect form data from 2nd parameter + form_data="" + for param in ${2}; do + form_data="$form_data -F \"$param\"" + done; + + eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" | jq -r "$jqexp" } # get script options @@ -80,12 +86,15 @@ case ${METHOD}_${#} in "open_3") call ${METHOD} "'$(printf '["%s",%s,%s]' "${1}" "${2}" "${3}")'" ;; # ${2} ${3} are numeric (funding, push) "open_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (funding) - "receive_2") call ${METHOD} "'$(printf '[%s,"%s"]' "${1}" "${2}")'" ;; # ${1} is numeric (amount to receive) + "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; # ${1} is numeric (amount to receive) + + + "receive_3") call ${METHOD} "'$(printf '[%s,"%s",%s]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount to receive) as is ${2} for expiry in seconds - "channel_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } end" ;; + "channel_"*) call ${METHOD} "'${PARAMS}'" "map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - "channels_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } ) end" ;; + "channels_"*) call ${METHOD} "'${PARAMS}'" "map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; "send_3") call ${METHOD} "'$(printf '[%s,"%s","%s"]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount of the payment) "send_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (amount overriding the payment request) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 74df84de7f..c7db13d5ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -16,7 +16,7 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.http.scaladsl.server.directives.Credentials +import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} import fr.acinq.eclair.channel._ @@ -69,7 +69,10 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M // register an actor that feeds the queue when a payment is received appKit.system.actorOf(Props(new Actor { override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) - def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) } + + def receive: Receive = { + case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) + } })) Flow[Message] @@ -79,12 +82,16 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M } val route: Route = { - respondWithDefaultHeaders(customHeaders){ - handleExceptions(apiExceptionHandler){ - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator){ _ => + respondWithDefaultHeaders(customHeaders) { + handleExceptions(apiExceptionHandler) { + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => post { - path("getinfo") { complete(getInfoResponse) } ~ - path("help") { complete(help.mkString) } ~ + path("getinfo") { + complete(getInfoResponse) + } ~ + path("help") { + complete(help.mkString) + } ~ path("connect") { formFields("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => complete(connect(s"$nodeId@$addr")) @@ -126,8 +133,12 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M complete(channelInfo(channelId)) } } ~ - path("allnodes") { complete(allnodes()) } ~ - path("allchannels") { complete(allchannels()) } ~ + path("allnodes") { + complete(allnodes()) + } ~ + path("allchannels") { + complete(allchannels()) + } ~ path("allupdates") { formFields("nodeId".as[PublicKey].?) { nodeId_opt => complete(allupdates(nodeId_opt)) @@ -178,6 +189,7 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M } } } + } def connect(uri: String): Future[String] = { @@ -254,7 +266,7 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden - case _ => throw ApiError("send", "Wrong params list, call 'help' to know more about it") + case _ => throw ApiError("send", "Wrong params list, call 'help' to know more about it") } val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry From 746a5daf9ab08af64610a16faabfd5e958d4a82e Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 15:17:29 +0100 Subject: [PATCH 22/75] Remove Json4s generic unmarshaller from scope --- .../fr/acinq/eclair/api/JsonSerializers.scala | 10 +++++- .../fr/acinq/eclair/api/NewService.scala | 33 ++++++++++--------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 454d13077a..fe018030c0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -18,7 +18,11 @@ package fr.acinq.eclair.api import java.net.InetSocketAddress +import akka.http.scaladsl.model.MediaType +import akka.http.scaladsl.model.MediaTypes._ import com.google.common.net.HostAndPort +import de.heikoseeberger.akkahttpjson4s.Json4sSupport +import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, OutPoint, Transaction} import fr.acinq.eclair.channel.State @@ -32,6 +36,8 @@ import fr.acinq.eclair.{ShortChannelId, UInt64} import org.json4s.JsonAST._ import org.json4s.{CustomKeySerializer, CustomSerializer, jackson} +import scala.collection.immutable + /** * JSON Serializers. * Note: in general, deserialization does not need to be implemented. @@ -145,7 +151,7 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format = Nil) })) -trait WithJsonSerializers { +object JsonSupport extends Json4sSupport { implicit val serialization = jackson.Serialization @@ -174,4 +180,6 @@ trait WithJsonSerializers { new DirectionSerializer + new PaymentRequestSerializer + implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index c7db13d5ae..84fb373af4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -3,7 +3,6 @@ package fr.acinq.eclair.api import akka.util.Timeout import akka.pattern._ import akka.http.scaladsl.server._ -import de.heikoseeberger.akkahttpjson4s.Json4sSupport.{ShouldWritePretty, marshaller, unmarshaller} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} @@ -27,11 +26,15 @@ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentReques import fr.acinq.eclair.router.{ChannelDesc, RouteNotFound, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import grizzled.slf4j.Logging - import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait NewService extends Directives with WithJsonSerializers with Logging with MetaService { +trait NewService extends Directives with Logging with MetaService { + + import JsonSupport.formats + import JsonSupport.serialization + // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 + import JsonSupport.marshaller def appKit: Kit @@ -42,13 +45,12 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M implicit val ec = appKit.system.dispatcher implicit val mat: ActorMaterializer implicit val timeout = Timeout(60 seconds) - implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) val apiExceptionHandler = ExceptionHandler { - case e: ApiError => + case e: IllegalApiParams => e.thr.foreach(thr => logger.warn(s"caught $thr")) complete(StatusCodes.BadRequest, e.msg) case t: Throwable => @@ -93,10 +95,8 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M complete(help.mkString) } ~ path("connect") { - formFields("nodeId".as[PublicKey], "address".as[NodeAddress]) { (nodeId, addr) => - complete(connect(s"$nodeId@$addr")) - } ~ formFields("uri") { uri => - complete(connect(uri)) + formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) => + complete(connect(nodeId, host, port, uri)) } } ~ path("open") { @@ -189,11 +189,12 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M } } } - } - def connect(uri: String): Future[String] = { - (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] + def connect(nodeId_opt: Option[PublicKey], host_opt:Option[String], port_opt: Option[Int], uri_opt: Option[String]): Future[String] = (nodeId_opt, host_opt, port_opt, uri_opt) match { + case (None, None, None, Some(uri)) => (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] + case (Some(nodeId), Some(host), Some(port), None) => (appKit.switchboard ? Peer.Connect(NodeURI.parse(s"$nodeId@$host:$port"))).mapTo[String] + case _ => throw IllegalApiParams("connect") } def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { @@ -258,7 +259,7 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M case (None, Some(amountMsat), Some(invoice)) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse] case (Some(nodeId), Some(amountMsat), None) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse] - case _ => throw ApiError("findroute", "Wrong params list, call 'help' to know more about it") + case _ => throw IllegalApiParams("findroute") } def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[PaymentResult] = { @@ -266,7 +267,7 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden - case _ => throw ApiError("send", "Wrong params list, call 'help' to know more about it") + case _ => throw IllegalApiParams("send") } val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry @@ -280,7 +281,7 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[Boolean] = (paymentHash_opt, invoice_opt) match { case (Some(ph), None) => (appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean] case (None, Some(invoice)) => (appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean] - case _ => throw ApiError("checkpayment", "Wrong params list, call 'help' to know more about it") + case _ => throw IllegalApiParams("checkpayment", "Wrong params list, call 'help' to know more about it") } def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { @@ -362,6 +363,6 @@ trait NewService extends Directives with WithJsonSerializers with Logging with M case _ => akka.pattern.after(1 second, using = appKit.system.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force } - case class ApiError(apiMethod: String, msg: String, thr: Option[Throwable] = None) extends RuntimeException(s"Error calling $apiMethod: $msg") + case class IllegalApiParams(apiMethod: String, msg: String = "Wrong params list, call 'help' to know more about it", thr: Option[Throwable] = None) extends RuntimeException(s"Error calling $apiMethod: $msg") } \ No newline at end of file From 3b20d1e490300bc1586d3589c459a4daf5c7e508 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 15:28:50 +0100 Subject: [PATCH 23/75] [WIP] Update eclair-cli --- eclair-core/eclair-cli | 48 ++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index fac784925f..e93687ec72 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -33,7 +33,7 @@ Full documentation at: " call() { jqexp='.' # 'if .error == null then .result else .error.message end' # override default jq parsing expression - if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi + # if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi # set password if [ -z ${PASSWORD} ]; then auth="eclair-cli"; else auth="eclair-cli:"${PASSWORD}; fi @@ -42,8 +42,9 @@ call() { for param in ${2}; do form_data="$form_data -F \"$param\"" done; + echo "FORM_DATA: $form_data" - eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" | jq -r "$jqexp" + eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" # | jq -r "$jqexp" } # get script options @@ -62,16 +63,6 @@ shift $(($OPTIND - 1)) METHOD=${1} shift 1 -# Create a JSON Array containing the remaining program args as QUOTED STRINGS, separated with a `,` character -PARAMS="" -i=1 -for arg in "${@}"; do - if [ $i -eq 1 ]; then PARAMS=$(printf '"%s"' "$arg"); - else PARAMS=$(printf '%s,"%s"' "$PARAMS" "$arg"); - fi - let "i++" -done; -PARAMS="[${PARAMS}]" # Whatever the arguments provided to eclair-cli, a call to the API will be sent. Let it fail! case ${METHOD}_${#} in @@ -80,34 +71,31 @@ case ${METHOD}_${#} in echo -e "\nAvailable commands:\n" call "help" [] ;; - "connect_3") call ${METHOD} "'$(printf '["%s","%s",%s]' "${1}" "${2}" "${3}")'" ;; # ${3} is numeric + "getinfo_0") call ${METHOD} "" ;; - "open_4") call ${METHOD} "'$(printf '["%s",%s,%s,%s]' "${1}" "${2}" "${3}" "${4}")'" ;; # ${2} ${3} ${4} are numeric (funding, push, flags) - "open_3") call ${METHOD} "'$(printf '["%s",%s,%s]' "${1}" "${2}" "${3}")'" ;; # ${2} ${3} are numeric (funding, push) - "open_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (funding) + "connect_1") call ${METHOD} "$(printf uri=%s ${1})" ;; + "connect_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf host=%s ${2})" "$(printf port=%s ${3})" " ;; - "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; # ${1} is numeric (amount to receive) + "open_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" " ;; + "open_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" " ;; + "open_4") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" " ;; + "open_5") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" "$(printf fundingFeerateSatByte=%s ${5})" " ;; + "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; + "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; - "receive_3") call ${METHOD} "'$(printf '[%s,"%s",%s]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount to receive) as is ${2} for expiry in seconds - - "channel_"*) call ${METHOD} "'${PARAMS}'" "map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - - "channels_"*) call ${METHOD} "'${PARAMS}'" "map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - - "send_3") call ${METHOD} "'$(printf '[%s,"%s","%s"]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount of the payment) - "send_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (amount overriding the payment request) + "send_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "send_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "send_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf invoice=%s ${3})" " ;; "audit_2") call ${METHOD} "'$(printf '[%s,%s]' "${1}" "${2}")'" ;; # ${1} and ${2} are numeric (unix timestamps) "networkfees_2") call ${METHOD} "'$(printf '[%s,%s]' "${1}" "${2}")'" ;; # ${1} and ${2} are numeric (unix timestamps) + "findroute_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + *) # Default case. - # Sends the method and, for parameters, use the JSON table containing the remaining args. - # - # NOTE: Arguments will be sent as QUOTED STRING so if this particular API call requires an INT param, - # this call will fail. In that case, a specific rule for that method MUST be set and the ${PARAMS} JSON array can not be used. - call ${METHOD} "'${PARAMS}'" ;; + displayhelp ; exit 1 ;; esac From 8ba0e0a3a821ceba7ccdb80ba07e81f6e9493fd5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 15:53:17 +0100 Subject: [PATCH 24/75] Adjust eclair-cli for the new APIs, output the invoice for /receive endpoint --- eclair-core/eclair-cli | 65 +++++++++++++------ .../fr/acinq/eclair/api/NewService.scala | 4 +- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index e93687ec72..c8cb983072 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -44,7 +44,7 @@ call() { done; echo "FORM_DATA: $form_data" - eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" # | jq -r "$jqexp" + eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" | jq -r "$jqexp" } # get script options @@ -71,31 +71,58 @@ case ${METHOD}_${#} in echo -e "\nAvailable commands:\n" call "help" [] ;; - "getinfo_0") call ${METHOD} "" ;; + "getinfo_0") call ${METHOD} "" ;; - "connect_1") call ${METHOD} "$(printf uri=%s ${1})" ;; - "connect_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf host=%s ${2})" "$(printf port=%s ${3})" " ;; + "connect_1") call ${METHOD} "$(printf uri=%s ${1})" ;; + "connect_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf host=%s ${2})" "$(printf port=%s ${3})" " ;; - "open_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" " ;; - "open_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" " ;; - "open_4") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" " ;; - "open_5") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" "$(printf fundingFeerateSatByte=%s ${5})" " ;; + "open_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" " ;; + "open_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" " ;; + "open_4") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" " ;; + "open_5") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" "$(printf fundingFeerateSatByte=%s ${5})" " ;; - "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; - "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; + "close_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; + "close_2") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf scriptPubKey=%s ${2})" " ;; + "forceclose_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; - "send_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "send_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "send_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf invoice=%s ${3})" " ;; + "updaterelayfee_3") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf feeBaseMsat=%s ${2})" "$(printf feeProportionalMillionths=%s ${3})" " ;; - "audit_2") call ${METHOD} "'$(printf '[%s,%s]' "${1}" "${2}")'" ;; # ${1} and ${2} are numeric (unix timestamps) - - "networkfees_2") call ${METHOD} "'$(printf '[%s,%s]' "${1}" "${2}")'" ;; # ${1} and ${2} are numeric (unix timestamps) + "peers_0") call ${METHOD} "" ;; - "findroute_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "channel_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; - *) # Default case. - displayhelp ; exit 1 ;; + "channels_1") call ${METHOD} "$(printf toRemoteNodeId=%s ${1})" ;; + + "allnodes_0") call ${METHOD} "" ;; + + "allchannels_0") call ${METHOD} "" ;; + + "allupdates_0") call ${METHOD} "" ;; + "allupdates_1") call ${METHOD} "$(printf nodeId=%s ${1})" ;; + + "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; + "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; + + "send_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "send_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "send_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; + + "parseinvoice_1") call ${METHOD} "$(printf invoice=%s ${1})" ;; + + "findroute_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + + "checkpayment_1") call "checkpayment" "$(printf invoice=%s ${1})" ;; + "checkpaymentbyhash_1") call "checkpayment" "$(printf paymentHash=%s ${1})" ;; # calls checkinvoice but using the paymentHash instead of the invoice + + "audit_0") call ${METHOD} "" ;; + "audit_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; + + "networkfees_0") call ${METHOD} "" ;; + "networkfees_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; + + "channelstats_0") call ${METHOD} "" ;; + + *) displayhelp ; exit 1 ;; # Default case. esac diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 84fb373af4..804c81a233 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -250,7 +250,9 @@ trait NewService extends Directives with Logging with MetaService { } def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = { - (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire)).mapTo[String] + (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire)).mapTo[PaymentRequest].map { pr => + PaymentRequest.write(pr) + } } def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Future[RouteResponse] = (nodeId_opt, amount_opt, invoice_opt) match { From 1eedeb054accee7dab57d9e2643ead2138863ce0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 16:32:19 +0100 Subject: [PATCH 25/75] [WIP] eclair-cli --- eclair-core/eclair-cli | 3 ++- .../src/main/scala/fr/acinq/eclair/api/NewService.scala | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index c8cb983072..dc337d552e 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -34,6 +34,7 @@ call() { jqexp='.' # 'if .error == null then .result else .error.message end' # override default jq parsing expression # if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi + # set password if [ -z ${PASSWORD} ]; then auth="eclair-cli"; else auth="eclair-cli:"${PASSWORD}; fi @@ -42,7 +43,7 @@ call() { for param in ${2}; do form_data="$form_data -F \"$param\"" done; - echo "FORM_DATA: $form_data" + #echo "FORM_DATA: $form_data" eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" | jq -r "$jqexp" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 804c81a233..6e1aa1d709 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -92,7 +92,7 @@ trait NewService extends Directives with Logging with MetaService { complete(getInfoResponse) } ~ path("help") { - complete(help.mkString) + complete(help) } ~ path("connect") { formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) => From cecb6bcdd8a04520960bc407124f929f7bba2baa Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 18:15:27 +0100 Subject: [PATCH 26/75] Fix help command in eclair-cli --- eclair-core/eclair-cli | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index dc337d552e..0832363e55 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -7,7 +7,7 @@ command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborti FULL_OUTPUT='false' URL='http://localhost:8080' -PASSWORD='foobar' +PASSWORD='' # -------------------- METHODS @@ -22,6 +22,7 @@ With COMMAND is one of the command listed by \e[01;33meclair-cli help\e[0m. -v Outputs full json returned by the API Examples: + eclair-cli help display available commands eclair-cli -a localhost:1234 peers list the peers eclair-cli close 006fb... closes the channel with id 006fb... @@ -70,7 +71,7 @@ case ${METHOD}_${#} in ""_*) displayhelp ;; "help"*) displayhelp echo -e "\nAvailable commands:\n" - call "help" [] ;; + call "help" "" ;; "getinfo_0") call ${METHOD} "" ;; From c9e573a4c232052472866f80bd645160bb96647e Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 13 Mar 2019 18:50:50 +0100 Subject: [PATCH 27/75] Improve error handling via rejections --- .../eclair/api/FormParamExtractors.scala | 10 - .../fr/acinq/eclair/api/NewService.scala | 255 ++++++++++-------- 2 files changed, 137 insertions(+), 128 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index d1b1c99c3d..dc27b2de5a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -4,7 +4,6 @@ import akka.http.scaladsl.unmarshalling.Unmarshaller import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.payment.PaymentRequest -import fr.acinq.eclair.wire.NodeAddress import scala.util.{Failure, Success, Try} object FormParamExtractors { @@ -18,15 +17,6 @@ object FormParamExtractors { } } - // assumes IPv4 like XXX.YYY.ZZZ.EEE:1234 - implicit val inetAddressUnmarshaller: Unmarshaller[String, NodeAddress] = Unmarshaller.strict { rawAddress => - val Array(host: String, port: String) = rawAddress.split(":") - NodeAddress.fromParts(host, port.toInt) match { - case Success(address) => address - case Failure(thr) => throw thr - } - } - implicit val binaryDataUnmarshaller: Unmarshaller[String, BinaryData] = Unmarshaller.strict { hex => BinaryData(hex) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 6e1aa1d709..2078692105 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -11,7 +11,7 @@ import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.http.scaladsl.model.HttpMethods.POST -import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes} import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} import akka.http.scaladsl.model.ws.{Message, TextMessage} @@ -44,7 +44,7 @@ trait NewService extends Directives with Logging with MetaService { implicit val ec = appKit.system.dispatcher implicit val mat: ActorMaterializer - implicit val timeout = Timeout(60 seconds) + implicit val timeout = Timeout(60 seconds) // used by akka ask // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) @@ -58,11 +58,17 @@ trait NewService extends Directives with Logging with MetaService { complete(StatusCodes.InternalServerError, s"Error: $t") } + val apiRejectionHandler = RejectionHandler.newBuilder() + .handle { + case UnknownMethodRejection => complete(StatusCodes.BadRequest, "Wrong method") + case UnknownParamsRejection(msg) => complete(StatusCodes.BadRequest, msg) + } + .result() + val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: `Access-Control-Allow-Methods`(POST) :: `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil - lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { // create a flow transforming a queue of string -> string @@ -83,118 +89,127 @@ trait NewService extends Directives with Logging with MetaService { .map(TextMessage.apply) } + val timeoutResponse: HttpRequest => HttpResponse = { r => + HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """) + } + val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => - post { - path("getinfo") { - complete(getInfoResponse) - } ~ - path("help") { - complete(help) - } ~ - path("connect") { - formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) => - complete(connect(nodeId, host, port, uri)) - } - } ~ - path("open") { - formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(channelId, scriptPubKey_opt)) - } - } ~ - path("forceclose") { - formFields(channelIdNamedParameter) { channelId => - complete(forceClose(channelId.toString)) - } - } ~ - path("updaterelayfee") { - formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(peersInfo()) - } ~ - path("channels") { - formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - formFields(channelIdNamedParameter) { channelId => - complete(channelInfo(channelId)) - } - } ~ - path("allnodes") { - complete(allnodes()) - } ~ - path("allchannels") { - complete(allchannels()) - } ~ - path("allupdates") { - formFields("nodeId".as[PublicKey].?) { nodeId_opt => - complete(allupdates(nodeId_opt)) - } - } ~ - path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - formFields("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findroute") { - formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => - complete(findRoute(nodeId, amount, invoice)) - } - } ~ - path("send") { - formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => - complete(send(nodeId, amountMsat, paymentHash, invoice)) - } - } ~ - path("checkpayment") { - formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => - complete(checkpayment(paymentHash, invoice)) - } - } ~ - path("audit") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(audit(from, to)) - } - } ~ - path("networkfees") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(networkFees(from, to)) - } - } ~ - path("channelstats") { - complete(channelStats()) - } ~ - path("ws") { - handleWebSocketMessages(makeSocketHandler) + handleRejections(apiRejectionHandler){ + withRequestTimeoutResponse(timeoutResponse){ + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => + post { + path("getinfo") { + complete(getInfoResponse) + } ~ + path("help") { + complete(help) + } ~ + path("connect") { + formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) => + connect(nodeId, host, port, uri) + } + } ~ + path("open") { + formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(close(channelId, scriptPubKey_opt)) + } + } ~ + path("forceclose") { + formFields(channelIdNamedParameter) { channelId => + complete(forceClose(channelId.toString)) + } + } ~ + path("updaterelayfee") { + formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(peersInfo()) + } ~ + path("channels") { + formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + formFields(channelIdNamedParameter) { channelId => + complete(channelInfo(channelId)) + } + } ~ + path("allnodes") { + complete(allnodes()) + } ~ + path("allchannels") { + complete(allchannels()) + } ~ + path("allupdates") { + formFields("nodeId".as[PublicKey].?) { nodeId_opt => + complete(allupdates(nodeId_opt)) + } + } ~ + path("receive") { + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + formFields("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findroute") { + formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => + findRoute(nodeId, amount, invoice) + } + } ~ + path("send") { + formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + complete(send(nodeId, amountMsat, paymentHash, invoice)) + } + } ~ + path("checkpayment") { + formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + checkpayment(paymentHash, invoice) + } + } ~ + path("audit") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(audit(from, to)) + } + } ~ + path("networkfees") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) + } ~ + path(Segment) { _ => reject() } } + } } } } } } - def connect(nodeId_opt: Option[PublicKey], host_opt:Option[String], port_opt: Option[Int], uri_opt: Option[String]): Future[String] = (nodeId_opt, host_opt, port_opt, uri_opt) match { - case (None, None, None, Some(uri)) => (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] - case (Some(nodeId), Some(host), Some(port), None) => (appKit.switchboard ? Peer.Connect(NodeURI.parse(s"$nodeId@$host:$port"))).mapTo[String] - case _ => throw IllegalApiParams("connect") + def connect(nodeId_opt: Option[PublicKey], host_opt:Option[String], port_opt: Option[Int], uri_opt: Option[String]): Route = (nodeId_opt, host_opt, port_opt, uri_opt) match { + case (None, None, None, Some(uri)) => complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) + case (Some(nodeId), Some(host), Some(port), None) => complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(s"$nodeId@$host:$port"))).mapTo[String]) + case _ => reject(UnknownParamsRejection("Wrong arguments for 'connect'")) } def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { @@ -255,35 +270,35 @@ trait NewService extends Directives with Logging with MetaService { } } - def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Future[RouteResponse] = (nodeId_opt, amount_opt, invoice_opt) match { + def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Route = (nodeId_opt, amount_opt, invoice_opt) match { case (None, None, Some(invoice@PaymentRequest(_, Some(amountMsat), _, targetNodeId, _, _))) => - (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat.toLong, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse] + complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat.toLong, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse]) case (None, Some(amountMsat), Some(invoice)) => - (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse] - case (Some(nodeId), Some(amountMsat), None) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse] - case _ => throw IllegalApiParams("findroute") + complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse]) + case (Some(nodeId), Some(amountMsat), None) => complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse]) + case _ => reject(UnknownParamsRejection("Wrong params for method 'findroute'")) } - def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[PaymentResult] = { + def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Route = { val (targetNodeId, paymentHash, amountMsat) = (nodeId_opt, amount_opt, paymentHash_opt, invoice_opt) match { case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden - case _ => throw IllegalApiParams("send") + case _ => return reject(UnknownParamsRejection("Wrong params for method 'send'")) } val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry - (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { + complete((appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { case s: PaymentSucceeded => s case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) - } + }) } - def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[Boolean] = (paymentHash_opt, invoice_opt) match { - case (Some(ph), None) => (appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean] - case (None, Some(invoice)) => (appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean] - case _ => throw IllegalApiParams("checkpayment", "Wrong params list, call 'help' to know more about it") + def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Route = (paymentHash_opt, invoice_opt) match { + case (Some(ph), None) => complete((appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean]) + case (None, Some(invoice)) => complete((appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean]) + case _ => reject(UnknownParamsRejection("Wrong params for method 'checkpayment'")) } def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { @@ -366,5 +381,9 @@ trait NewService extends Directives with Logging with MetaService { } case class IllegalApiParams(apiMethod: String, msg: String = "Wrong params list, call 'help' to know more about it", thr: Option[Throwable] = None) extends RuntimeException(s"Error calling $apiMethod: $msg") + case object UnknownMethodRejection extends Rejection + case class UnknownParamsRejection(message: String) extends Rejection + case class RpcValidationRejection(message: String) extends Rejection + case class ExceptionRejection(message: String) extends Rejection } \ No newline at end of file From 31ce0c688ea3297664e1dd05f860d377c178f923 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 14 Mar 2019 16:04:29 +0100 Subject: [PATCH 28/75] Fix transactions unit test To reconstruct the tx commit number we need to take the lower 24 bits of the locktime field. --- .../scala/fr/acinq/eclair/transactions/TransactionsSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index d81b2c8d1a..8e32ae7e12 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -215,7 +215,7 @@ class TransactionsSpec extends FunSuite with Logging { assert(getCommitTxNumber(commitTx.tx, true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) val hash = Crypto.sha256(localPaymentPriv.publicKey.toBin ++ remotePaymentPriv.publicKey.toBin) val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL - val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | commitTx.tx.lockTime + val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.tx.lockTime & 0xffffff) assert((check ^ num) == commitTxNumber) } val (htlcTimeoutTxs, htlcSuccessTxs) = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) From 305219d929390b10a0f8af543d3706f7988f291a Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 14 Mar 2019 16:19:19 +0100 Subject: [PATCH 29/75] Finish merging master --- .../eclair/api/FormParamExtractors.scala | 16 +++++------- .../fr/acinq/eclair/api/NewService.scala | 26 ++++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index dc27b2de5a..4196d41f84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -1,31 +1,29 @@ package fr.acinq.eclair.api import akka.http.scaladsl.unmarshalling.Unmarshaller -import fr.acinq.bitcoin.BinaryData +import fr.acinq.bitcoin.{ByteVector32} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.payment.PaymentRequest +import scodec.bits.ByteVector import scala.util.{Failure, Success, Try} object FormParamExtractors { implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey => Try { - PublicKey(rawPubKey) + PublicKey(ByteVector.fromValidHex(rawPubKey)) } match { case Success(key) => key case Failure(exception) => throw exception } } - implicit val binaryDataUnmarshaller: Unmarshaller[String, BinaryData] = Unmarshaller.strict { hex => - BinaryData(hex) + implicit val binaryDataUnmarshaller: Unmarshaller[String, ByteVector] = Unmarshaller.strict { str => + ByteVector.fromValidHex(str) } - implicit val sha256HashUnmarshaller: Unmarshaller[String, BinaryData] = binaryDataUnmarshaller.map { bin => - bin.size match { - case 32 => bin - case _ => throw new IllegalArgumentException(s"$bin is not a valid SHA256 hash") - } + implicit val sha256HashUnmarshaller: Unmarshaller[String, ByteVector32] = Unmarshaller.strict { bin => + ByteVector32.fromValidHex(bin) } implicit val bolt11Unmarshaller: Unmarshaller[String, PaymentRequest] = Unmarshaller.strict { rawRequest => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 2078692105..72f75446c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -4,7 +4,7 @@ import akka.util.Timeout import akka.pattern._ import akka.http.scaladsl.server._ import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} import FormParamExtractors._ @@ -26,6 +26,8 @@ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentReques import fr.acinq.eclair.router.{ChannelDesc, RouteNotFound, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import grizzled.slf4j.Logging +import scodec.bits.ByteVector + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -47,7 +49,7 @@ trait NewService extends Directives with Logging with MetaService { implicit val timeout = Timeout(60 seconds) // used by akka ask // a named and typed URL parameter used across several routes, 32-bytes hex-encoded - val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller) + val channelIdNamedParameter = "channelId".as[ByteVector32](sha256HashUnmarshaller) val apiExceptionHandler = ExceptionHandler { case e: IllegalApiParams => @@ -118,7 +120,7 @@ trait NewService extends Directives with Logging with MetaService { } } ~ path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => complete(close(channelId, scriptPubKey_opt)) } } ~ @@ -172,12 +174,12 @@ trait NewService extends Directives with Logging with MetaService { } } ~ path("send") { - formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => + formFields("amountMsat".as[Long].?, "paymentHash".as[ByteVector32](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => complete(send(nodeId, amountMsat, paymentHash, invoice)) } } ~ path("checkpayment") { - formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => + formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => checkpayment(paymentHash, invoice) } } ~ @@ -221,7 +223,7 @@ trait NewService extends Directives with Logging with MetaService { channelFlags = flags.map(_.toByte))).mapTo[String] } - def close(channelId: BinaryData, scriptPubKey: Option[BinaryData]): Future[String] = { + def close(channelId: ByteVector32, scriptPubKey: Option[ByteVector]): Future[String] = { sendToChannel(channelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String] } @@ -240,16 +242,16 @@ trait NewService extends Directives with Logging with MetaService { def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = toRemoteNode match { case Some(pk) => for { - channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys) + channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector, PublicKey]].map(_.filter(_._2 == pk).keys) channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) } yield channels case None => for { - channels_id <- (appKit.register ? 'channels).mapTo[Map[BinaryData, ActorRef]].map(_.keys) + channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector, ActorRef]].map(_.keys) channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) } yield channels } - def channelInfo(channelId: BinaryData): Future[RES_GETINFO] = { + def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] = { sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO] } @@ -279,7 +281,7 @@ trait NewService extends Directives with Logging with MetaService { case _ => reject(UnknownParamsRejection("Wrong params for method 'findroute'")) } - def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Route = { + def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[ByteVector32], invoice_opt: Option[PaymentRequest]): Route = { val (targetNodeId, paymentHash, amountMsat) = (nodeId_opt, amount_opt, paymentHash_opt, invoice_opt) match { case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) @@ -295,7 +297,7 @@ trait NewService extends Directives with Logging with MetaService { }) } - def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Route = (paymentHash_opt, invoice_opt) match { + def checkpayment(paymentHash_opt: Option[ByteVector32], invoice_opt: Option[PaymentRequest]): Route = (paymentHash_opt, invoice_opt) match { case (Some(ph), None) => complete((appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean]) case (None, Some(invoice)) => complete((appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean]) case _ => reject(UnknownParamsRejection("Wrong params for method 'checkpayment'")) @@ -335,7 +337,7 @@ trait NewService extends Directives with Logging with MetaService { def sendToChannel(channelIdentifier: String, request: Any): Future[Any] = for { fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request)) - .recoverWith { case _ => Future(Register.Forward(BinaryData(channelIdentifier), request)) } + .recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) } .recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) } res <- appKit.register ? fwdReq } yield res From 837ac03dd9cb85d02b56b3a730653e1c2530c792 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 14 Mar 2019 18:34:18 +0100 Subject: [PATCH 30/75] Accept shortChannelId in /close, finish porting the test from the previous APIs --- .../eclair/api/FormParamExtractors.scala | 22 ++-- .../fr/acinq/eclair/api/NewService.scala | 14 +-- eclair-core/src/test/resources/api/close | 5 +- eclair-core/src/test/resources/api/getinfo | 12 +- eclair-core/src/test/resources/api/help | 4 - eclair-core/src/test/resources/api/peers | 14 +-- ...ServiceSpec.scala => ApiServiceSpec.scala} | 111 +++++++----------- 7 files changed, 61 insertions(+), 121 deletions(-) delete mode 100644 eclair-core/src/test/resources/api/help rename eclair-core/src/test/scala/fr/acinq/eclair/api/{JsonRpcServiceSpec.scala => ApiServiceSpec.scala} (64%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index 4196d41f84..5e15506b84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -1,21 +1,16 @@ package fr.acinq.eclair.api import akka.http.scaladsl.unmarshalling.Unmarshaller -import fr.acinq.bitcoin.{ByteVector32} +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.payment.PaymentRequest import scodec.bits.ByteVector -import scala.util.{Failure, Success, Try} object FormParamExtractors { implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey => - Try { - PublicKey(ByteVector.fromValidHex(rawPubKey)) - } match { - case Success(key) => key - case Failure(exception) => throw exception - } + PublicKey(ByteVector.fromValidHex(rawPubKey)) } implicit val binaryDataUnmarshaller: Unmarshaller[String, ByteVector] = Unmarshaller.strict { str => @@ -27,12 +22,11 @@ object FormParamExtractors { } implicit val bolt11Unmarshaller: Unmarshaller[String, PaymentRequest] = Unmarshaller.strict { rawRequest => - Try { - PaymentRequest.read(rawRequest) - } match { - case Success(req) => req - case Failure(exception) => throw exception - } + PaymentRequest.read(rawRequest) + } + + implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => + ShortChannelId(str) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 72f75446c9..b40aaaabaa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -50,11 +50,9 @@ trait NewService extends Directives with Logging with MetaService { // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelIdNamedParameter = "channelId".as[ByteVector32](sha256HashUnmarshaller) + val shortChannelIdNamedParameter = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) val apiExceptionHandler = ExceptionHandler { - case e: IllegalApiParams => - e.thr.foreach(thr => logger.warn(s"caught $thr")) - complete(StatusCodes.BadRequest, e.msg) case t: Throwable => logger.error(s"API call failed with cause=${t.getMessage}") complete(StatusCodes.InternalServerError, s"Error: $t") @@ -120,8 +118,8 @@ trait NewService extends Directives with Logging with MetaService { } } ~ path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(channelId, scriptPubKey_opt)) + formFields(channelIdNamedParameter.?, shortChannelIdNamedParameter.?, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId_opt, shortChannelId_opt, scriptPubKey_opt) => + close(channelId_opt, shortChannelId_opt, scriptPubKey_opt) } } ~ path("forceclose") { @@ -223,8 +221,10 @@ trait NewService extends Directives with Logging with MetaService { channelFlags = flags.map(_.toByte))).mapTo[String] } - def close(channelId: ByteVector32, scriptPubKey: Option[ByteVector]): Future[String] = { - sendToChannel(channelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String] + def close(channelId_opt: Option[ByteVector32], shortChannelId_opt: Option[ShortChannelId], scriptPubKey: Option[ByteVector]): Route = (channelId_opt, shortChannelId_opt) match { + case (Some(channelId), None) => complete(sendToChannel(channelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String]) + case (None, Some(shortChannelId)) => complete(sendToChannel(shortChannelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String]) + case _ => reject(UnknownParamsRejection("Wrong params for method 'close'")) } def forceClose(channelId: String): Future[String] = { diff --git a/eclair-core/src/test/resources/api/close b/eclair-core/src/test/resources/api/close index b7ab40e6c8..06b1bfe627 100644 --- a/eclair-core/src/test/resources/api/close +++ b/eclair-core/src/test/resources/api/close @@ -1,4 +1 @@ -{ - "result" : "03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0", - "id" : "eclair-node" -} \ No newline at end of file +"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0" \ No newline at end of file diff --git a/eclair-core/src/test/resources/api/getinfo b/eclair-core/src/test/resources/api/getinfo index 53cb58243f..89b8fa275a 100644 --- a/eclair-core/src/test/resources/api/getinfo +++ b/eclair-core/src/test/resources/api/getinfo @@ -1,11 +1 @@ -{ - "result" : { - "publicAddresses" : [ "localhost:9731" ], - "alias" : "alice", - "port" : 9735, - "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", - "blockHeight" : 123456, - "nodeId" : "03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0" - }, - "id" : "eclair-node" -} \ No newline at end of file +{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","port":9735,"chainHash":{"bytes":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"},"blockHeight":123456,"publicAddresses":["localhost:9731"]} \ No newline at end of file diff --git a/eclair-core/src/test/resources/api/help b/eclair-core/src/test/resources/api/help deleted file mode 100644 index d7a3a511cf..0000000000 --- a/eclair-core/src/test/resources/api/help +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result" : [ "connect (uri): open a secure connection to a lightning node", "connect (nodeId, host, port): open a secure connection to a lightning node", "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", "peers: list existing local peers", "channels: list existing local channels", "channels (nodeId): list existing local channels to a particular nodeId", "channel (channelId): retrieve detailed information about a given channel", "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", "allnodes: list all known nodes", "allchannels: list all known channels", "allupdates: list all channels updates", "allupdates (nodeId): list all channels updates for this nodeId", "receive (amountMsat, description): generate a payment request for a given amount", "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", "findroute (paymentRequest): returns nodes and channels of the route if there is any", "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", "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", "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", "close (channelId): close a channel", "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", "audit: list all send/received/relayed payments", "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", "networkfees: list all network fees paid to the miners, by transaction", "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", "getinfo: returns info about the blockchain and this node", "help: display this message" ], - "id" : "eclair-node" -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/api/peers b/eclair-core/src/test/resources/api/peers index 7b2def6703..3e12eddaa8 100644 --- a/eclair-core/src/test/resources/api/peers +++ b/eclair-core/src/test/resources/api/peers @@ -1,13 +1 @@ -{ - "result" : [ { - "nodeId" : "03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0", - "state" : "CONNECTED", - "address" : "localhost:9731", - "channels" : 1 - }, { - "nodeId" : "039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585", - "state" : "DISCONNECTED", - "channels" : 1 - } ], - "id" : "eclair-node" -} \ No newline at end of file +[{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"localhost:9731","channels":1},{"nodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","state":"DISCONNECTED","channels":1}] \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala similarity index 64% rename from eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 41cd680175..7e17bbeb8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -17,33 +17,36 @@ package fr.acinq.eclair.api -import java.io.{File, FileOutputStream} +import java.io.{File, FileOutputStream, PrintWriter} +import java.nio.file.{Files, Path, Paths, StandardOpenOption} -import akka.NotUsed -import akka.actor.{Actor, Props, Scheduler} +import akka.actor.{Actor, ActorSystem, Props, Scheduler} +import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ -import akka.http.scaladsl.model.headers.BasicHttpCredentials -import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} -import akka.stream.scaladsl.Flow -import de.heikoseeberger.akkahttpjson4s.Json4sSupport.{marshaller, unmarshaller} -import fr.acinq.eclair.Kit -import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.TestWallet -import fr.acinq.eclair.channel.Register.ForwardShortId +import fr.acinq.eclair.{Kit, TestConstants} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import org.json4s.Formats -import org.json4s.JsonAST.{JInt, JString} +import TestConstants._ +import akka.http.scaladsl.model.headers.BasicHttpCredentials +import akka.http.scaladsl.server.Route +import akka.stream.ActorMaterializer +import fr.acinq.eclair.channel.Register.ForwardShortId +import org.json4s.{Formats, JValue} import org.json4s.jackson.Serialization -import org.scalatest.FunSuite +import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source import scala.util.Try -class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { +class ApiServiceSpec extends FunSuite with ScalatestRouteTest { + + implicit val formats = JsonSupport.formats + implicit val serialization = JsonSupport.serialization + implicit val marshaller = JsonSupport.marshaller + implicit val unmarshaller = JsonSupport.unmarshaller implicit val routeTestTimeout = RouteTestTimeout(3 seconds) @@ -65,24 +68,22 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { override def receive: Receive = { case _ => } } - class MockService(kit: Kit = defaultMockKit) extends Service { + class MockService(kit: Kit = defaultMockKit) extends NewService { + override def getInfoResponse: Future[GetInfoResponse] = Future.successful(???) override def appKit: Kit = kit - override val scheduler: Scheduler = system.scheduler - override def password: String = "mock" - override val socketHandler: Flow[Message, TextMessage.Strict, NotUsed] = makeSocketHandler(system)(materializer) + override implicit val mat: ActorMaterializer = ActorMaterializer() } test("API service should handle failures correctly"){ val mockService = new MockService - import mockService.{formats, serialization} // no auth - Post("/", JsonRPCBody(method = "help", params = Seq.empty)) ~> + Post("/help") ~> Route.seal(mockService.route) ~> check { assert(handled) @@ -90,9 +91,8 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { } // wrong auth - Post("/", JsonRPCBody(method = "help", params = Seq.empty)) ~> + Post("/help") ~> addCredentials(BasicHttpCredentials("", mockService.password+"what!")) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) @@ -100,29 +100,27 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { } // correct auth but wrong URL - Post("/mistake", JsonRPCBody(method = "help", params = Seq.empty)) ~> + Post("/mistake") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) assert(status == NotFound) } - // wrong rpc method - Post("/", JsonRPCBody(method = "open_not_really", params = Seq.empty)) ~> + // wrong param type + Post("/channel", FormData(Map("channelId" -> "hey")).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) assert(status == BadRequest) + assert(entityAs[String].contains("The form field 'channelId' was malformed")) } // wrong params - Post("/", JsonRPCBody(method = "open", params = Seq(JInt(123), JString("abc")))) ~> + Post("/connect", FormData("nodeId" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87", "uri" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) @@ -133,22 +131,17 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { test("'help' should respond with a help message") { val mockService = new MockService - import mockService.{formats, serialization} - val postBody = JsonRPCBody(method = "help", params = Seq.empty) - - Post("/", postBody) ~> + Post("/help") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) assert(status == OK) - val resp = entityAs[JsonRPCRes] + val resp = entityAs[String] matchTestJson("help", false ,resp) } - } test("'peers' should ask the switchboard for current known peers") { @@ -182,22 +175,13 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { })) )) - import mockService.{formats, serialization} - - val postBody = JsonRPCBody(method = "peers", params = Seq.empty) - - Post("/", postBody) ~> + Post("/peers") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) assert(status == OK) - val response = entityAs[JsonRPCRes] - val peerInfos = response.result.asInstanceOf[Seq[Map[String,String]]] - assert(peerInfos.size == 2) - assert(peerInfos.head.get("nodeId") == Some(Alice.nodeParams.nodeId.toString)) - assert(peerInfos.head.get("state") == Some("CONNECTED")) + val response = entityAs[String] matchTestJson("peers", false, response) } } @@ -213,19 +197,17 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { publicAddresses = Alice.nodeParams.publicAddresses )) } - import mockService.{formats, serialization} - val postBody = JsonRPCBody(method = "getinfo", params = Seq.empty) - Post("/", postBody) ~> + Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) assert(status == OK) - val resp = entityAs[JsonRPCRes] - assert(resp.result.toString.contains(Alice.nodeParams.nodeId.toString)) + val resp = entityAs[String] + assert(resp.toString.contains(Alice.nodeParams.nodeId.toString)) matchTestJson("getinfo", false ,resp) } } @@ -243,36 +225,29 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { })) )) - import mockService.{formats, serialization} - - - val postBody = JsonRPCBody(method = "close", params = Seq(JString(shortChannelIdSerialized))) - Post("/", postBody) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) assert(status == OK) - val resp = entityAs[JsonRPCRes] - assert(resp.result.toString.contains(Alice.nodeParams.nodeId.toString)) + val resp = entityAs[String] + assert(resp.contains(Alice.nodeParams.nodeId.toString)) matchTestJson("close", false ,resp) } } - private def readFileAsString(stream: File): Try[String] = Try(Source.fromFile(stream).mkString) + private def matchTestJson(apiName: String, overWrite: Boolean, response: String)(implicit formats: Formats) = { + val p = Paths.get(s"eclair-core/src/test/resources/api/$apiName") - private def matchTestJson(rpcMethod: String, overWrite: Boolean, response: JsonRPCRes)(implicit formats: Formats) = { - val responseContent = Serialization.writePretty(response) - val resourceName = s"/api/$rpcMethod" - val resourceFile = new File(getClass.getResource(resourceName).toURI.toURL.getFile) if(overWrite) { - new FileOutputStream(resourceFile).write(responseContent.getBytes) + Files.writeString(p, response) assert(false, "'overWrite' should be false before commit") } else { - val expectedResponse = readFileAsString(resourceFile).getOrElse(throw new IllegalArgumentException(s"Mock file for '$resourceName' does not exist, please use 'overWrite' first.")) - assert(responseContent == expectedResponse, s"Test mock for $rpcMethod did not match the expected response") + val expectedResponse = Source.fromFile(p.toUri).mkString + assert(response == expectedResponse, s"Test mock for $apiName did not match the expected response") } } From 5ff5042f2d892c9f8e6974f48ebac59db6576720 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 14 Mar 2019 18:35:05 +0100 Subject: [PATCH 31/75] Add mock file for /help --- eclair-core/src/test/resources/api/help | 1 + 1 file changed, 1 insertion(+) create mode 100644 eclair-core/src/test/resources/api/help diff --git a/eclair-core/src/test/resources/api/help b/eclair-core/src/test/resources/api/help new file mode 100644 index 0000000000..9d58627ae2 --- /dev/null +++ b/eclair-core/src/test/resources/api/help @@ -0,0 +1 @@ +["connect (uri): open a secure connection to a lightning node","connect (nodeId, host, port): open a secure connection to a lightning node","open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced","updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel","peers: list existing local peers","channels: list existing local channels","channels (nodeId): list existing local channels to a particular nodeId","channel (channelId): retrieve detailed information about a given channel","channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)","allnodes: list all known nodes","allchannels: list all known channels","allupdates: list all channels updates","allupdates (nodeId): list all channels updates for this nodeId","receive (amountMsat, description): generate a payment request for a given amount","receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires","parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request","findroute (paymentRequest): returns nodes and channels of the route if there is any","findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any","findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any","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","send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount","close (channelId): close a channel","close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey","forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)","checkpayment (paymentHash): returns true if the payment has been received, false otherwise","checkpayment (paymentRequest): returns true if the payment has been received, false otherwise","audit: list all send/received/relayed payments","audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)","networkfees: list all network fees paid to the miners, by transaction","networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)","getinfo: returns info about the blockchain and this node","help: display this message"] \ No newline at end of file From ec10feb40aedc587abf215619c85e59983ce0502 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 11:29:55 +0100 Subject: [PATCH 32/75] Do not return Route(s) from the implementation of API methods, add test for /connect --- .../fr/acinq/eclair/api/NewService.scala | 75 +++++++++---------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 43 ++++++++++- 2 files changed, 73 insertions(+), 45 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index b40aaaabaa..fb92a8a2f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -107,8 +107,10 @@ trait NewService extends Directives with Logging with MetaService { complete(help) } ~ path("connect") { - formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) => - connect(nodeId, host, port, uri) + formFields("uri".as[String]) { uri => + complete(connect(uri)) + } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => + complete(connect(s"$nodeId@$host:$port")) } } ~ path("open") { @@ -118,8 +120,10 @@ trait NewService extends Directives with Logging with MetaService { } } ~ path("close") { - formFields(channelIdNamedParameter.?, shortChannelIdNamedParameter.?, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId_opt, shortChannelId_opt, scriptPubKey_opt) => - close(channelId_opt, shortChannelId_opt, scriptPubKey_opt) + formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(close(Left(channelId), scriptPubKey_opt)) + } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => + complete(close(Right(shortChannelId), scriptPubKey_opt)) } } ~ path("forceclose") { @@ -167,18 +171,26 @@ trait NewService extends Directives with Logging with MetaService { } } ~ path("findroute") { - formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) => - findRoute(nodeId, amount, invoice) + formFields(, "invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(findRoute(nodeId, amount.toLong, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => complete(findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + } ~ formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => + complete(findRoute(nodeId, amount)) } } ~ path("send") { - formFields("amountMsat".as[Long].?, "paymentHash".as[ByteVector32](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) => - complete(send(nodeId, amountMsat, paymentHash, invoice)) + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => complete(send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo)) + } ~ formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + complete(send(nodeId, amountMsat, paymentHash)) } } ~ path("checkpayment") { - formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) => - checkpayment(paymentHash, invoice) + formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => + complete(checkpayment(paymentHash)) + } ~ formFields("invoice".as[PaymentRequest]) { invoice => + complete(checkpayment(invoice.paymentHash)) } } ~ path("audit") { @@ -206,10 +218,8 @@ trait NewService extends Directives with Logging with MetaService { } } - def connect(nodeId_opt: Option[PublicKey], host_opt:Option[String], port_opt: Option[Int], uri_opt: Option[String]): Route = (nodeId_opt, host_opt, port_opt, uri_opt) match { - case (None, None, None, Some(uri)) => complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) - case (Some(nodeId), Some(host), Some(port), None) => complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(s"$nodeId@$host:$port"))).mapTo[String]) - case _ => reject(UnknownParamsRejection("Wrong arguments for 'connect'")) + def connect(uri: String): Future[String] = { + (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] } def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { @@ -221,10 +231,8 @@ trait NewService extends Directives with Logging with MetaService { channelFlags = flags.map(_.toByte))).mapTo[String] } - def close(channelId_opt: Option[ByteVector32], shortChannelId_opt: Option[ShortChannelId], scriptPubKey: Option[ByteVector]): Route = (channelId_opt, shortChannelId_opt) match { - case (Some(channelId), None) => complete(sendToChannel(channelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String]) - case (None, Some(shortChannelId)) => complete(sendToChannel(shortChannelId.toString(), CMD_CLOSE(scriptPubKey)).mapTo[String]) - case _ => reject(UnknownParamsRejection("Wrong params for method 'close'")) + def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] = { + sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_CLOSE(scriptPubKey)).mapTo[String] } def forceClose(channelId: String): Future[String] = { @@ -272,35 +280,20 @@ trait NewService extends Directives with Logging with MetaService { } } - def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Route = (nodeId_opt, amount_opt, invoice_opt) match { - case (None, None, Some(invoice@PaymentRequest(_, Some(amountMsat), _, targetNodeId, _, _))) => - complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat.toLong, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse]) - case (None, Some(amountMsat), Some(invoice)) => - complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse]) - case (Some(nodeId), Some(amountMsat), None) => complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse]) - case _ => reject(UnknownParamsRejection("Wrong params for method 'findroute'")) + def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] = { + (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] } - def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[ByteVector32], invoice_opt: Option[PaymentRequest]): Route = { - val (targetNodeId, paymentHash, amountMsat) = (nodeId_opt, amount_opt, paymentHash_opt, invoice_opt) match { - case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount) - case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong) - case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden - case _ => return reject(UnknownParamsRejection("Wrong params for method 'send'")) - } - - val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry - - complete((appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { + def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[PaymentResult] = { + val sendPayment = SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) // TODO add minFinalCltvExpiry + (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { case s: PaymentSucceeded => s case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) - }) + } } - def checkpayment(paymentHash_opt: Option[ByteVector32], invoice_opt: Option[PaymentRequest]): Route = (paymentHash_opt, invoice_opt) match { - case (Some(ph), None) => complete((appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean]) - case (None, Some(invoice)) => complete((appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean]) - case _ => reject(UnknownParamsRejection("Wrong params for method 'checkpayment'")) + def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { + (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Boolean] } def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 7e17bbeb8e..44bf14e74e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -35,11 +35,10 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import org.json4s.{Formats, JValue} import org.json4s.jackson.Serialization import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} - +import fr.acinq.eclair.io.Peer import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source -import scala.util.Try class ApiServiceSpec extends FunSuite with ScalatestRouteTest { @@ -119,7 +118,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } // wrong params - Post("/connect", FormData("nodeId" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87", "uri" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735").toEntity) ~> + Post("/connect", FormData("urb" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> check { @@ -239,8 +238,44 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } } + test("'connect' method should accept an URI and a triple with nodeId/host/port") { + + val remoteNodeId = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" + val remoteHost = "93.137.102.239" + val remotePort = "9735" + val remoteUri = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735" + + val mockService = new MockService(defaultMockKit.copy( + switchboard = system.actorOf(Props(new {} with MockActor { + override def receive = { + case Peer.Connect(_) => sender() ! "connected" + } + })) + )) + + + Post("/connect", FormData("nodeId" -> remoteNodeId, "host" -> remoteHost, "port" -> remotePort).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + assert(entityAs[String] == "\"connected\"") + } + + Post("/connect", FormData("uri" -> remoteUri).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + println(entityAs[String]) + assert(entityAs[String] == "\"connected\"") + } + } + private def matchTestJson(apiName: String, overWrite: Boolean, response: String)(implicit formats: Formats) = { - val p = Paths.get(s"eclair-core/src/test/resources/api/$apiName") + val p = Paths.get(s"src/test/resources/api/$apiName") if(overWrite) { Files.writeString(p, response) From 53ec015e02c2b64d5e83ea7d601c287d14555133 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 12:27:33 +0100 Subject: [PATCH 33/75] Separate Service and EclairApiImpl --- .../main/scala/fr/acinq/eclair/Setup.scala | 7 +- .../scala/fr/acinq/eclair/api/EclairApi.scala | 224 +++++++++++++++ .../fr/acinq/eclair/api/NewService.scala | 262 ++++-------------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 41 ++- 4 files changed, 298 insertions(+), 236 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 6735f4f4c4..e80ac6bcd3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -31,7 +31,7 @@ import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} -import fr.acinq.eclair.api.{GetInfoResponse, NewService, Service} +import fr.acinq.eclair.api.{EclairApi, GetInfoResponse, NewService, Service} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher} @@ -265,7 +265,7 @@ class Setup(datadir: File, val api = if(config.getBoolean("api.use-new-version")){ new NewService { - override def appKit: Kit = kit + override val actorSystem = kit.system override val mat = materializer @@ -274,13 +274,14 @@ class Setup(datadir: File, if (p.isEmpty) throw EmptyAPIPasswordException else p } - override def getInfoResponse: Future[GetInfoResponse] = Future.successful( + override def eclairApi: EclairApi = new fr.acinq.eclair.api.EclairApiImpl(kit, Future.successful( GetInfoResponse(nodeId = nodeParams.nodeId, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue(), publicAddresses = nodeParams.publicAddresses)) + ) } } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala new file mode 100644 index 0000000000..b444ff7aaa --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -0,0 +1,224 @@ +package fr.acinq.eclair.api + +import akka.util.Timeout +import akka.pattern._ +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.eclair.{Kit, ShortChannelId} +import fr.acinq.eclair.io.{NodeURI, Peer} +import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.{NetworkFee, Stats} +import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} +import fr.acinq.eclair.payment.PaymentLifecycle._ +import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} +import fr.acinq.eclair.router.{ChannelDesc, RouteNotFound, RouteRequest, RouteResponse} +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} +import scodec.bits.ByteVector +import scala.concurrent.duration._ +import scala.concurrent.Future + +trait EclairApi { + + def connect(uri: String): Future[String] + + def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] + + def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] + + def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId]): Future[String] + + def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] + + def peersInfo(): Future[Iterable[PeerInfo]] + + def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] + + def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] + + def allnodes(): Future[Iterable[NodeAnnouncement]] + + def allchannels(): Future[Iterable[ChannelDesc]] + + def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] + + def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] + + def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] + + def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[PaymentResult] + + def checkpayment(paymentHash: ByteVector32): Future[Boolean] + + def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] + + def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] + + def channelStats(): Future[Seq[Stats]] + + def help(): List[String] + + def getInfoResponse(): Future[GetInfoResponse] + +} + +class EclairApiImpl(appKit: Kit, getInfo: Future[GetInfoResponse]) extends EclairApi { + + implicit val ec = appKit.system.dispatcher + implicit val timeout = Timeout(60 seconds) // used by akka ask + + override def connect(uri: String): Future[String] = { + (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] + } + + override def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { + (appKit.switchboard ? Peer.OpenChannel( + remoteNodeId = nodeId, + fundingSatoshis = Satoshi(fundingSatoshis), + pushMsat = pushMsat.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), + fundingTxFeeratePerKw_opt = fundingFeerateSatByte, + channelFlags = flags.map(_.toByte))).mapTo[String] + } + + override def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] = { + sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_CLOSE(scriptPubKey)).mapTo[String] + } + + override def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId]): Future[String] = { + sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_FORCECLOSE).mapTo[String] + } + + override def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] = { + sendToChannel(channelId, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String] + } + + override def peersInfo(): Future[Iterable[PeerInfo]] = for { + peers <- (appKit.switchboard ? 'peers).mapTo[Iterable[ActorRef]] + peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) + } yield peerinfos + + override def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = toRemoteNode match { + case Some(pk) => for { + channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector, PublicKey]].map(_.filter(_._2 == pk).keys) + channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + } yield channels + case None => for { + channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector, ActorRef]].map(_.keys) + channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + } yield channels + } + + override def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] = { + sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO] + } + + override def allnodes(): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]] + + override def allchannels(): Future[Iterable[ChannelDesc]] = { + (appKit.router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))) + } + + override def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] = nodeId match { + case None => (appKit.router ? 'updates).mapTo[Iterable[ChannelUpdate]] + case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) + } + + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = { + (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire)).mapTo[PaymentRequest].map { pr => + PaymentRequest.write(pr) + } + } + + override def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] = { + (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] + } + + override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[PaymentResult] = { + val sendPayment = SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) // TODO add minFinalCltvExpiry + (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { + case s: PaymentSucceeded => s + case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) + } + } + + override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { + (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Boolean] + } + + override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { + val (from, to) = (from_opt, to_opt) match { + case (Some(f), Some(t)) => (f, t) + case _ => (0L, Long.MaxValue) + } + + Future(AuditResponse( + sent = appKit.nodeParams.auditDb.listSent(from, to), + received = appKit.nodeParams.auditDb.listReceived(from, to), + relayed = appKit.nodeParams.auditDb.listRelayed(from, to) + )) + } + + override def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] = { + val (from, to) = (from_opt, to_opt) match { + case (Some(f), Some(t)) => (f, t) + case _ => (0L, Long.MaxValue) + } + + Future(appKit.nodeParams.auditDb.listNetworkFees(from, to)) + } + + override def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.auditDb.stats) + + /** + * Sends a request to a channel and expects a response + * + * @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) + * @param request + * @return + */ + def sendToChannel(channelIdentifier: String, request: Any): Future[Any] = + for { + fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request)) + .recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) } + .recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) } + res <- appKit.register ? fwdReq + } yield res + + override def getInfoResponse: Future[GetInfoResponse] = getInfo + + override def help = List( + "connect (uri): open a secure connection to a lightning node", + "connect (nodeId, host, port): open a secure connection to a lightning node", + "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", + "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", + "peers: list existing local peers", + "channels: list existing local channels", + "channels (nodeId): list existing local channels to a particular nodeId", + "channel (channelId): retrieve detailed information about a given channel", + "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", + "allnodes: list all known nodes", + "allchannels: list all known channels", + "allupdates: list all channels updates", + "allupdates (nodeId): list all channels updates for this nodeId", + "receive (amountMsat, description): generate a payment request for a given amount", + "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", + "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", + "findroute (paymentRequest): returns nodes and channels of the route if there is any", + "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", + "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", + "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", + "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", + "close (channelId): close a channel", + "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", + "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", + "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", + "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", + "audit: list all send/received/relayed payments", + "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", + "networkfees: list all network fees paid to the miners, by transaction", + "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", + "getinfo: returns info about the blockchain and this node", + "help: display this message") + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index fb92a8a2f4..b8a4733ebb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -1,12 +1,9 @@ package fr.acinq.eclair.api -import akka.util.Timeout -import akka.pattern._ import akka.http.scaladsl.server._ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.{Kit, ShortChannelId} -import fr.acinq.eclair.io.{NodeURI, Peer} import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} @@ -18,16 +15,9 @@ import akka.http.scaladsl.model.ws.{Message, TextMessage} import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.{NetworkFee, Stats} -import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} -import fr.acinq.eclair.router.{ChannelDesc, RouteNotFound, RouteRequest, RouteResponse} -import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import grizzled.slf4j.Logging import scodec.bits.ByteVector - import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -38,15 +28,13 @@ trait NewService extends Directives with Logging with MetaService { // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 import JsonSupport.marshaller - def appKit: Kit - - def getInfoResponse: Future[GetInfoResponse] - def password: String - implicit val ec = appKit.system.dispatcher + def eclairApi: EclairApi + + implicit val actorSystem: ActorSystem + implicit lazy val ec = actorSystem.dispatcher implicit val mat: ActorMaterializer - implicit val timeout = Timeout(60 seconds) // used by akka ask // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelIdNamedParameter = "channelId".as[ByteVector32](sha256HashUnmarshaller) @@ -60,8 +48,8 @@ trait NewService extends Directives with Logging with MetaService { val apiRejectionHandler = RejectionHandler.newBuilder() .handle { - case UnknownMethodRejection => complete(StatusCodes.BadRequest, "Wrong method") - case UnknownParamsRejection(msg) => complete(StatusCodes.BadRequest, msg) + case UnknownMethodRejection => complete(StatusCodes.BadRequest, "Wrong method or params combination") + case UnknownParamsRejection => complete(StatusCodes.BadRequest, "Wrong params combination") } .result() @@ -75,7 +63,7 @@ trait NewService extends Directives with Logging with MetaService { val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() // register an actor that feeds the queue when a payment is received - appKit.system.actorOf(Props(new Actor { + actorSystem.actorOf(Props(new Actor { override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) def receive: Receive = { @@ -93,6 +81,15 @@ trait NewService extends Directives with Logging with MetaService { HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """) } + def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { + case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) + case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force + } + + case object UnknownMethodRejection extends Rejection + case object UnknownParamsRejection extends Rejection + + val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { @@ -101,68 +98,70 @@ trait NewService extends Directives with Logging with MetaService { authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => post { path("getinfo") { - complete(getInfoResponse) + complete(eclairApi.getInfoResponse()) } ~ path("help") { - complete(help) + complete(eclairApi.help()) } ~ path("connect") { formFields("uri".as[String]) { uri => - complete(connect(uri)) + complete(eclairApi.connect(uri)) } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => - complete(connect(s"$nodeId@$host:$port")) + complete(eclairApi.connect(s"$nodeId@$host:$port")) } } ~ path("open") { formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) } } ~ path("close") { formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(close(Left(channelId), scriptPubKey_opt)) + complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => - complete(close(Right(shortChannelId), scriptPubKey_opt)) + complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) } } ~ path("forceclose") { formFields(channelIdNamedParameter) { channelId => - complete(forceClose(channelId.toString)) + complete(eclairApi.forceClose(Left(channelId))) + } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => + complete(eclairApi.forceClose(Right(shortChannelId))) } } ~ path("updaterelayfee") { formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(updateRelayFee(channelId.toString, feeBase, feeProportional)) + complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) } } ~ path("peers") { - complete(peersInfo()) + complete(eclairApi.peersInfo()) } ~ path("channels") { formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(channelsInfo(toRemoteNodeId_opt)) + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) } } ~ path("channel") { formFields(channelIdNamedParameter) { channelId => - complete(channelInfo(channelId)) + complete(eclairApi.channelInfo(channelId)) } } ~ path("allnodes") { - complete(allnodes()) + complete(eclairApi.allnodes()) } ~ path("allchannels") { - complete(allchannels()) + complete(eclairApi.allchannels()) } ~ path("allupdates") { formFields("nodeId".as[PublicKey].?) { nodeId_opt => - complete(allupdates(nodeId_opt)) + complete(eclairApi.allupdates(nodeId_opt)) } } ~ path("receive") { formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(receive(desc, amountMsat, expire)) + complete(eclairApi.receive(desc, amountMsat, expire)) } } ~ path("parseinvoice") { @@ -171,45 +170,49 @@ trait NewService extends Directives with Logging with MetaService { } } ~ path("findroute") { - formFields(, "invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(findRoute(nodeId, amount.toLong, invoice.routingInfo)) - case (invoice, Some(overrideAmount)) => complete(findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + case _ => reject(UnknownParamsRejection) } ~ formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => - complete(findRoute(nodeId, amount)) + complete(eclairApi.findRoute(nodeId, amount)) } } ~ path("send") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo)) - case (invoice, Some(overrideAmount)) => complete(send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo)) + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => + complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo)) + case _ => reject(UnknownParamsRejection) } ~ formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => - complete(send(nodeId, amountMsat, paymentHash)) + complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ path("checkpayment") { formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => - complete(checkpayment(paymentHash)) + complete(eclairApi.checkpayment(paymentHash)) } ~ formFields("invoice".as[PaymentRequest]) { invoice => - complete(checkpayment(invoice.paymentHash)) + complete(eclairApi.checkpayment(invoice.paymentHash)) } } ~ path("audit") { formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(audit(from, to)) + complete(eclairApi.audit(from, to)) } } ~ path("networkfees") { formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(networkFees(from, to)) + complete(eclairApi.networkFees(from, to)) } } ~ path("channelstats") { - complete(channelStats()) + complete(eclairApi.channelStats()) } ~ path("ws") { handleWebSocketMessages(makeSocketHandler) } ~ - path(Segment) { _ => reject() } + path(Segment) { _ => reject(UnknownMethodRejection) } } } } @@ -218,167 +221,4 @@ trait NewService extends Directives with Logging with MetaService { } } - def connect(uri: String): Future[String] = { - (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String] - } - - def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = { - (appKit.switchboard ? Peer.OpenChannel( - remoteNodeId = nodeId, - fundingSatoshis = Satoshi(fundingSatoshis), - pushMsat = pushMsat.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), - fundingTxFeeratePerKw_opt = fundingFeerateSatByte, - channelFlags = flags.map(_.toByte))).mapTo[String] - } - - def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] = { - sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_CLOSE(scriptPubKey)).mapTo[String] - } - - def forceClose(channelId: String): Future[String] = { - sendToChannel(channelId, CMD_FORCECLOSE).mapTo[String] - } - - def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] = { - sendToChannel(channelId, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String] - } - - def peersInfo(): Future[Iterable[PeerInfo]] = for { - peers <- (appKit.switchboard ? 'peers).mapTo[Iterable[ActorRef]] - peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) - } yield peerinfos - - def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = toRemoteNode match { - case Some(pk) => for { - channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector, PublicKey]].map(_.filter(_._2 == pk).keys) - channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) - } yield channels - case None => for { - channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector, ActorRef]].map(_.keys) - channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) - } yield channels - } - - def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] = { - sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO] - } - - def allnodes(): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]] - - def allchannels(): Future[Iterable[ChannelDesc]] = { - (appKit.router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))) - } - - def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] = nodeId match { - case None => (appKit.router ? 'updates).mapTo[Iterable[ChannelUpdate]] - case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) - } - - def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = { - (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire)).mapTo[PaymentRequest].map { pr => - PaymentRequest.write(pr) - } - } - - def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] = { - (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] - } - - def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[PaymentResult] = { - val sendPayment = SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) // TODO add minFinalCltvExpiry - (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { - case s: PaymentSucceeded => s - case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) - } - } - - def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { - (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Boolean] - } - - def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { - val (from, to) = (from_opt, to_opt) match { - case (Some(f), Some(t)) => (f, t) - case _ => (0L, Long.MaxValue) - } - - Future(AuditResponse( - sent = appKit.nodeParams.auditDb.listSent(from, to), - received = appKit.nodeParams.auditDb.listReceived(from, to), - relayed = appKit.nodeParams.auditDb.listRelayed(from, to) - )) - } - - def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] = { - val (from, to) = (from_opt, to_opt) match { - case (Some(f), Some(t)) => (f, t) - case _ => (0L, Long.MaxValue) - } - - Future(appKit.nodeParams.auditDb.listNetworkFees(from, to)) - } - - def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.auditDb.stats) - - /** - * Sends a request to a channel and expects a response - * - * @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) - * @param request - * @return - */ - def sendToChannel(channelIdentifier: String, request: Any): Future[Any] = - for { - fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request)) - .recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) } - .recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) } - res <- appKit.register ? fwdReq - } yield res - - def help = List( - "connect (uri): open a secure connection to a lightning node", - "connect (nodeId, host, port): open a secure connection to a lightning node", - "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", - "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", - "peers: list existing local peers", - "channels: list existing local channels", - "channels (nodeId): list existing local channels to a particular nodeId", - "channel (channelId): retrieve detailed information about a given channel", - "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", - "allnodes: list all known nodes", - "allchannels: list all known channels", - "allupdates: list all channels updates", - "allupdates (nodeId): list all channels updates for this nodeId", - "receive (amountMsat, description): generate a payment request for a given amount", - "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", - "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", - "findroute (paymentRequest): returns nodes and channels of the route if there is any", - "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", - "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", - "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", - "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", - "close (channelId): close a channel", - "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", - "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", - "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", - "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", - "audit: list all send/received/relayed payments", - "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", - "networkfees: list all network fees paid to the miners, by transaction", - "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", - "getinfo: returns info about the blockchain and this node", - "help: display this message") - - def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { - case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) - case _ => akka.pattern.after(1 second, using = appKit.system.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force - } - - case class IllegalApiParams(apiMethod: String, msg: String = "Wrong params list, call 'help' to know more about it", thr: Option[Throwable] = None) extends RuntimeException(s"Error calling $apiMethod: $msg") - case object UnknownMethodRejection extends Rejection - case class UnknownParamsRejection(message: String) extends Rejection - case class RpcValidationRejection(message: String) extends Rejection - case class ExceptionRejection(message: String) extends Rejection - } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 44bf14e74e..8a74158d4d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -33,9 +33,9 @@ import akka.http.scaladsl.server.Route import akka.stream.ActorMaterializer import fr.acinq.eclair.channel.Register.ForwardShortId import org.json4s.{Formats, JValue} -import org.json4s.jackson.Serialization import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.eclair.io.Peer + import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source @@ -49,7 +49,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { implicit val routeTestTimeout = RouteTestTimeout(3 seconds) - def defaultMockKit = Kit( + val defaultMockKit = Kit( nodeParams = Alice.nodeParams, system = system, watcher = system.actorOf(Props(new MockActor)), @@ -63,19 +63,26 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { wallet = new TestWallet ) + def defaultGetInfo = GetInfoResponse( + nodeId = Alice.nodeParams.nodeId, + alias = Alice.nodeParams.alias, + port = 9735, + chainHash = Alice.nodeParams.chainHash, + blockHeight = 123456, + publicAddresses = Alice.nodeParams.publicAddresses + ) + class MockActor extends Actor { override def receive: Receive = { case _ => } } - class MockService(kit: Kit = defaultMockKit) extends NewService { - - override def getInfoResponse: Future[GetInfoResponse] = Future.successful(???) - - override def appKit: Kit = kit + class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends NewService { + override def eclairApi: EclairApi = new EclairApiImpl(kit, Future.successful(getInfoResp)) override def password: String = "mock" - override implicit val mat: ActorMaterializer = ActorMaterializer() + override implicit val actorSystem: ActorSystem = system + override implicit val mat: ActorMaterializer = materializer } test("API service should handle failures correctly"){ @@ -104,7 +111,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { Route.seal(mockService.route) ~> check { assert(handled) - assert(status == NotFound) + assert(status == BadRequest) } // wrong param type @@ -114,7 +121,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { check { assert(handled) assert(status == BadRequest) - assert(entityAs[String].contains("The form field 'channelId' was malformed")) +// assert(entityAs[String].contains("The form field 'channelId' was malformed")) } // wrong params @@ -129,7 +136,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } test("'help' should respond with a help message") { - val mockService = new MockService + val mockService = new MockService() Post("/help") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> @@ -186,17 +193,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } test("'getinfo' response should include this node ID") { - val mockService = new {} with MockService { - override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse( - nodeId = Alice.nodeParams.nodeId, - alias = Alice.nodeParams.alias, - port = 9735, - chainHash = Alice.nodeParams.chainHash, - blockHeight = 123456, - publicAddresses = Alice.nodeParams.publicAddresses - )) - } - + val mockService = new MockService() Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> From 8c3d7d843a887fd3d79cc15ea13bb18b74ba2e45 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 13:56:07 +0100 Subject: [PATCH 34/75] Fix ClassCastException when mapping the channels responses to ByteVector instead of ByteVector32 --- .../src/main/scala/fr/acinq/eclair/api/EclairApi.scala | 6 +++--- .../src/main/scala/fr/acinq/eclair/api/NewService.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala index b444ff7aaa..82766c8449 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -99,12 +99,12 @@ class EclairApiImpl(appKit: Kit, getInfo: Future[GetInfoResponse]) extends Eclai override def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] = toRemoteNode match { case Some(pk) => for { - channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector, PublicKey]].map(_.filter(_._2 == pk).keys) + channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys) channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) } yield channels case None => for { - channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector, ActorRef]].map(_.keys) - channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys) + channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toHex, CMD_GETINFO).mapTo[RES_GETINFO])) } yield channels } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index b8a4733ebb..928d092cdc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -42,7 +42,7 @@ trait NewService extends Directives with Logging with MetaService { val apiExceptionHandler = ExceptionHandler { case t: Throwable => - logger.error(s"API call failed with cause=${t.getMessage}") + logger.error(s"API call failed with cause=${t.getMessage}", t) complete(StatusCodes.InternalServerError, s"Error: $t") } From 72cf2e87b54165c53bac7706ac93497da4104c97 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 14:04:21 +0100 Subject: [PATCH 35/75] Re-enable output processing with JQ in eclair-cli --- eclair-core/eclair-cli | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 0832363e55..1978d20c61 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -32,9 +32,9 @@ Full documentation at: " # Executes a JSON RPC call to a node listening on ${URL} call() { - jqexp='.' # 'if .error == null then .result else .error.message end' + jqexp='.' # override default jq parsing expression - # if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi + if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi # set password if [ -z ${PASSWORD} ]; then auth="eclair-cli"; @@ -44,7 +44,6 @@ call() { for param in ${2}; do form_data="$form_data -F \"$param\"" done; - #echo "FORM_DATA: $form_data" eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" | jq -r "$jqexp" } @@ -92,9 +91,10 @@ case ${METHOD}_${#} in "peers_0") call ${METHOD} "" ;; - "channel_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; + "channel_1") call ${METHOD} "$(printf channelId=%s ${1})" ". | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }" ;; - "channels_1") call ${METHOD} "$(printf toRemoteNodeId=%s ${1})" ;; + "channels_0") call ${METHOD} "" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; + "channels_1") call ${METHOD} "$(printf toRemoteNodeId=%s ${1})" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; "allnodes_0") call ${METHOD} "" ;; From 7b6c020fe10e9da7eb0e99a05adabea0d1d99cb2 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 14:17:45 +0100 Subject: [PATCH 36/75] Add ByteVector32 Serializer to JsonSupport scope --- .../src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 8075d2212a..347702301a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -35,9 +35,6 @@ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{ShortChannelId, UInt64} import org.json4s.JsonAST._ import org.json4s.{CustomKeySerializer, CustomSerializer, jackson} - -import scala.collection.immutable -import org.json4s.{CustomKeySerializer, CustomSerializer} import scodec.bits.ByteVector /** @@ -163,6 +160,7 @@ object JsonSupport extends Json4sSupport { implicit val formats = org.json4s.DefaultFormats + new ByteVectorSerializer + + new ByteVector32Serializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + From 1fe65f42317b7e88f167d3c996a95eec14d75461 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 14:23:14 +0100 Subject: [PATCH 37/75] Move out /help from EclairApi --- .../scala/fr/acinq/eclair/api/EclairApi.scala | 37 ------------------ .../fr/acinq/eclair/api/NewService.scala | 38 ++++++++++++++++++- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala index 82766c8449..30e16cf3cd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -56,8 +56,6 @@ trait EclairApi { def channelStats(): Future[Seq[Stats]] - def help(): List[String] - def getInfoResponse(): Future[GetInfoResponse] } @@ -186,39 +184,4 @@ class EclairApiImpl(appKit: Kit, getInfo: Future[GetInfoResponse]) extends Eclai override def getInfoResponse: Future[GetInfoResponse] = getInfo - override def help = List( - "connect (uri): open a secure connection to a lightning node", - "connect (nodeId, host, port): open a secure connection to a lightning node", - "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", - "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", - "peers: list existing local peers", - "channels: list existing local channels", - "channels (nodeId): list existing local channels to a particular nodeId", - "channel (channelId): retrieve detailed information about a given channel", - "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", - "allnodes: list all known nodes", - "allchannels: list all known channels", - "allupdates: list all channels updates", - "allupdates (nodeId): list all channels updates for this nodeId", - "receive (amountMsat, description): generate a payment request for a given amount", - "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", - "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", - "findroute (paymentRequest): returns nodes and channels of the route if there is any", - "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", - "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", - "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", - "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", - "close (channelId): close a channel", - "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", - "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", - "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", - "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", - "audit: list all send/received/relayed payments", - "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", - "networkfees: list all network fees paid to the miners, by transaction", - "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", - "getinfo: returns info about the blockchain and this node", - "help: display this message") - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 928d092cdc..c4ba5b655e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -101,7 +101,7 @@ trait NewService extends Directives with Logging with MetaService { complete(eclairApi.getInfoResponse()) } ~ path("help") { - complete(eclairApi.help()) + complete(help) } ~ path("connect") { formFields("uri".as[String]) { uri => @@ -221,4 +221,40 @@ trait NewService extends Directives with Logging with MetaService { } } + val help = List( + "connect (uri): open a secure connection to a lightning node", + "connect (nodeId, host, port): open a secure connection to a lightning node", + "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", + "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", + "peers: list existing local peers", + "channels: list existing local channels", + "channels (nodeId): list existing local channels to a particular nodeId", + "channel (channelId): retrieve detailed information about a given channel", + "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", + "allnodes: list all known nodes", + "allchannels: list all known channels", + "allupdates: list all channels updates", + "allupdates (nodeId): list all channels updates for this nodeId", + "receive (amountMsat, description): generate a payment request for a given amount", + "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", + "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", + "findroute (paymentRequest): returns nodes and channels of the route if there is any", + "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", + "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", + "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", + "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", + "close (channelId): close a channel", + "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", + "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", + "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", + "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", + "audit: list all send/received/relayed payments", + "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", + "networkfees: list all network fees paid to the miners, by transaction", + "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", + "getinfo: returns info about the blockchain and this node", + "help: display this message") + + } \ No newline at end of file From 8268e70c3596e2ba5939186d53635a67d48fefe1 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 14:25:27 +0100 Subject: [PATCH 38/75] Rename config key and make new API enabled by default --- eclair-core/src/main/resources/reference.conf | 2 +- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 3c17f11351..672cc0a2bd 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -13,7 +13,7 @@ eclair { binding-ip = "127.0.0.1" port = 8080 password = "" // password for basic auth, must be non empty if json-rpc api is enabled - use-new-version = false + use-old-api = false } watcher-type = "bitcoind" // other *experimental* values include "electrum" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index e80ac6bcd3..24c43fd18c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -262,7 +262,7 @@ class Setup(datadir: File, _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") implicit val materializer = ActorMaterializer() - val api = if(config.getBoolean("api.use-new-version")){ + val api = if(config.getBoolean("api.use-old-api")){ new NewService { override val actorSystem = kit.system From e8069d5719dc9affe482434c4abaf5f46ea1c84b Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 17:05:53 +0100 Subject: [PATCH 39/75] Fix usage of the deprecation config key (use-old-api) --- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 24c43fd18c..8aff78fbba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -31,7 +31,7 @@ import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} -import fr.acinq.eclair.api.{EclairApi, GetInfoResponse, NewService, Service} +import fr.acinq.eclair.api._ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher} @@ -262,7 +262,7 @@ class Setup(datadir: File, _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") implicit val materializer = ActorMaterializer() - val api = if(config.getBoolean("api.use-old-api")){ + val api = if(!config.getBoolean("api.use-old-api")) { new NewService { override val actorSystem = kit.system @@ -274,7 +274,7 @@ class Setup(datadir: File, if (p.isEmpty) throw EmptyAPIPasswordException else p } - override def eclairApi: EclairApi = new fr.acinq.eclair.api.EclairApiImpl(kit, Future.successful( + override def eclairApi: EclairApi = new EclairApiImpl(kit, Future.successful( GetInfoResponse(nodeId = nodeParams.nodeId, alias = nodeParams.alias, port = config.getInt("server.port"), From 6884725934b8b89fe4c2a215f7f1b5117f7cfb2d Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 17:09:11 +0100 Subject: [PATCH 40/75] Rework getInfo parameter passing, update the mock for it --- .../main/scala/fr/acinq/eclair/Setup.scala | 40 ++++++++----------- .../scala/fr/acinq/eclair/api/EclairApi.scala | 4 +- eclair-core/src/test/resources/api/getinfo | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 8aff78fbba..b9c6d731db 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -262,28 +262,28 @@ class Setup(datadir: File, _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") implicit val materializer = ActorMaterializer() - val api = if(!config.getBoolean("api.use-old-api")) { + val getInfo = GetInfoResponse(nodeId = nodeParams.nodeId, + alias = nodeParams.alias, + port = config.getInt("server.port"), + chainHash = nodeParams.chainHash, + blockHeight = Globals.blockCount.intValue(), + publicAddresses = nodeParams.publicAddresses) + + val api = if (!config.getBoolean("api.use-old-api")) { new NewService { - override val actorSystem = kit.system + override val actorSystem = kit.system - override val mat = materializer + override val mat = materializer - override val password = { - val p = config.getString("api.password") - if (p.isEmpty) throw EmptyAPIPasswordException else p - } + override val password = { + val p = config.getString("api.password") + if (p.isEmpty) throw EmptyAPIPasswordException else p + } - override def eclairApi: EclairApi = new EclairApiImpl(kit, Future.successful( - GetInfoResponse(nodeId = nodeParams.nodeId, - alias = nodeParams.alias, - port = config.getInt("server.port"), - chainHash = nodeParams.chainHash, - blockHeight = Globals.blockCount.intValue(), - publicAddresses = nodeParams.publicAddresses)) - ) + override def eclairApi: EclairApi = new EclairApiImpl(kit, getInfo) - } + } } else { new Service { @@ -294,13 +294,7 @@ class Setup(datadir: File, if (p.isEmpty) throw EmptyAPIPasswordException else p } - override def getInfoResponse: Future[GetInfoResponse] = Future.successful( - GetInfoResponse(nodeId = nodeParams.nodeId, - alias = nodeParams.alias, - port = config.getInt("server.port"), - chainHash = nodeParams.chainHash, - blockHeight = Globals.blockCount.intValue(), - publicAddresses = nodeParams.publicAddresses)) + override def getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo) override def appKit: Kit = kit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala index 30e16cf3cd..0667b13d1d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -60,7 +60,7 @@ trait EclairApi { } -class EclairApiImpl(appKit: Kit, getInfo: Future[GetInfoResponse]) extends EclairApi { +class EclairApiImpl (appKit: Kit, getInfo: GetInfoResponse) extends EclairApi { implicit val ec = appKit.system.dispatcher implicit val timeout = Timeout(60 seconds) // used by akka ask @@ -182,6 +182,6 @@ class EclairApiImpl(appKit: Kit, getInfo: Future[GetInfoResponse]) extends Eclai res <- appKit.register ? fwdReq } yield res - override def getInfoResponse: Future[GetInfoResponse] = getInfo + override def getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo) } diff --git a/eclair-core/src/test/resources/api/getinfo b/eclair-core/src/test/resources/api/getinfo index 89b8fa275a..f168177ed3 100644 --- a/eclair-core/src/test/resources/api/getinfo +++ b/eclair-core/src/test/resources/api/getinfo @@ -1 +1 @@ -{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","port":9735,"chainHash":{"bytes":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"},"blockHeight":123456,"publicAddresses":["localhost:9731"]} \ No newline at end of file +{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","port":9735,"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":123456,"publicAddresses":["localhost:9731"]} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 8a74158d4d..e000d8deb9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -35,8 +35,6 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import org.json4s.{Formats, JValue} import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.eclair.io.Peer - -import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source @@ -77,7 +75,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends NewService { - override def eclairApi: EclairApi = new EclairApiImpl(kit, Future.successful(getInfoResp)) + override def eclairApi: EclairApi = new EclairApiImpl(kit, getInfoResp) override def password: String = "mock" From 9ecda96b4c7cafefcf70e0858cb611c3c92c2f30 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 17:47:54 +0100 Subject: [PATCH 41/75] Remove default case for non matched routes --- .../src/main/scala/fr/acinq/eclair/api/NewService.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index c4ba5b655e..3553db40fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -23,10 +23,10 @@ import scala.concurrent.duration._ trait NewService extends Directives with Logging with MetaService { - import JsonSupport.formats - import JsonSupport.serialization // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 import JsonSupport.marshaller + import JsonSupport.formats + import JsonSupport.serialization def password: String @@ -89,7 +89,6 @@ trait NewService extends Directives with Logging with MetaService { case object UnknownMethodRejection extends Rejection case object UnknownParamsRejection extends Rejection - val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { @@ -211,8 +210,7 @@ trait NewService extends Directives with Logging with MetaService { } ~ path("ws") { handleWebSocketMessages(makeSocketHandler) - } ~ - path(Segment) { _ => reject(UnknownMethodRejection) } + } } } } From 2ebec3b053d32035c8a35c53fd44a5c538ab84b5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 18:09:25 +0100 Subject: [PATCH 42/75] Separate findRoute and send API to remove ambiguity (and improve error responses) --- eclair-core/eclair-cli | 10 ++++++---- .../scala/fr/acinq/eclair/api/NewService.scala | 17 +++++++++++------ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 ++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 1978d20c61..ebecd7ee48 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -106,13 +106,15 @@ case ${METHOD}_${#} in "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; - "send_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "send_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "send_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; + "sendToInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "sendToInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "sendToNode_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; "parseinvoice_1") call ${METHOD} "$(printf invoice=%s ${1})" ;; - "findroute_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "findRouteByInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "findRouteByInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "findRouteByNode_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; "checkpayment_1") call "checkpayment" "$(printf invoice=%s ${1})" ;; "checkpaymentbyhash_1") call "checkpayment" "$(printf paymentHash=%s ${1})" ;; # calls checkinvoice but using the paymentHash instead of the invoice diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 3553db40fe..f1faa328a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -168,23 +168,28 @@ trait NewService extends Directives with Logging with MetaService { complete(invoice) } } ~ - path("findroute") { + path("findRouteByInvoice") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) - case _ => reject(UnknownParamsRejection) - } ~ formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) + } + } ~ path("findRouteByNode") { + formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => complete(eclairApi.findRoute(nodeId, amount)) } } ~ - path("send") { + path("sendToInvoice") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo)) case (invoice, Some(overrideAmount)) => complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo)) - case _ => reject(UnknownParamsRejection) - } ~ formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendToNode") { + formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index e000d8deb9..a1f7e21041 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -109,7 +109,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { Route.seal(mockService.route) ~> check { assert(handled) - assert(status == BadRequest) + assert(status == NotFound) } // wrong param type @@ -119,7 +119,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { check { assert(handled) assert(status == BadRequest) -// assert(entityAs[String].contains("The form field 'channelId' was malformed")) + assert(entityAs[String].contains("The form field 'channelId' was malformed")) } // wrong params From f2cea92dbc43692188acc6e1e9fe4b8cf7428841 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 18:20:16 +0100 Subject: [PATCH 43/75] Use minFinalCltvExpiry from the invoice when sending a payment --- .../src/main/scala/fr/acinq/eclair/api/EclairApi.scala | 9 ++++++--- .../src/main/scala/fr/acinq/eclair/api/NewService.scala | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala index 0667b13d1d..484a3fda9f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -46,7 +46,7 @@ trait EclairApi { def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] - def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[PaymentResult] + def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[PaymentResult] def checkpayment(paymentHash: ByteVector32): Future[Boolean] @@ -131,8 +131,11 @@ class EclairApiImpl (appKit: Kit, getInfo: GetInfoResponse) extends EclairApi { (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] } - override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[PaymentResult] = { - val sendPayment = SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) // TODO add minFinalCltvExpiry + override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[PaymentResult] = { + val sendPayment = minFinalCltvExpiry match { + case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv) + case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) + } (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { case s: PaymentSucceeded => s case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index f1faa328a5..e07b3f1a8b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -182,9 +182,9 @@ trait NewService extends Directives with Logging with MetaService { path("sendToInvoice") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => - complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo)) + complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) case (invoice, Some(overrideAmount)) => - complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo)) + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) } } ~ From 6ab9e03be8e7462df5c706b0d324080c02593da5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 18:20:47 +0100 Subject: [PATCH 44/75] Formatting --- .../fr/acinq/eclair/api/NewService.scala | 15 +++++----- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 29 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index e07b3f1a8b..93726fd43d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -87,13 +87,14 @@ trait NewService extends Directives with Logging with MetaService { } case object UnknownMethodRejection extends Rejection + case object UnknownParamsRejection extends Rejection val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { - handleRejections(apiRejectionHandler){ - withRequestTimeoutResponse(timeoutResponse){ + handleRejections(apiRejectionHandler) { + withRequestTimeoutResponse(timeoutResponse) { authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => post { path("getinfo") { @@ -126,7 +127,7 @@ trait NewService extends Directives with Logging with MetaService { formFields(channelIdNamedParameter) { channelId => complete(eclairApi.forceClose(Left(channelId))) } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => - complete(eclairApi.forceClose(Right(shortChannelId))) + complete(eclairApi.forceClose(Right(shortChannelId))) } } ~ path("updaterelayfee") { @@ -175,10 +176,10 @@ trait NewService extends Directives with Logging with MetaService { case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) } } ~ path("findRouteByNode") { - formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) - } - } ~ + formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ path("sendToInvoice") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index a1f7e21041..dd9e14e1fa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -51,13 +51,13 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { nodeParams = Alice.nodeParams, system = system, watcher = system.actorOf(Props(new MockActor)), - paymentHandler = system.actorOf(Props(new MockActor)), - register = system.actorOf(Props(new MockActor)), - relayer = system.actorOf(Props(new MockActor)), + paymentHandler = system.actorOf(Props(new MockActor)), + register = system.actorOf(Props(new MockActor)), + relayer = system.actorOf(Props(new MockActor)), router = system.actorOf(Props(new MockActor)), - switchboard = system.actorOf(Props(new MockActor)), - paymentInitiator = system.actorOf(Props(new MockActor)), - server = system.actorOf(Props(new MockActor)), + switchboard = system.actorOf(Props(new MockActor)), + paymentInitiator = system.actorOf(Props(new MockActor)), + server = system.actorOf(Props(new MockActor)), wallet = new TestWallet ) @@ -71,7 +71,9 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { ) class MockActor extends Actor { - override def receive: Receive = { case _ => } + override def receive: Receive = { + case _ => + } } class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends NewService { @@ -83,7 +85,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override implicit val mat: ActorMaterializer = materializer } - test("API service should handle failures correctly"){ + test("API service should handle failures correctly") { val mockService = new MockService // no auth @@ -96,7 +98,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { // wrong auth Post("/help") ~> - addCredentials(BasicHttpCredentials("", mockService.password+"what!")) ~> + addCredentials(BasicHttpCredentials("", mockService.password + "what!")) ~> Route.seal(mockService.route) ~> check { assert(handled) @@ -143,7 +145,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(handled) assert(status == OK) val resp = entityAs[String] - matchTestJson("help", false ,resp) + matchTestJson("help", false, resp) } } @@ -202,7 +204,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(status == OK) val resp = entityAs[String] assert(resp.toString.contains(Alice.nodeParams.nodeId.toString)) - matchTestJson("getinfo", false ,resp) + matchTestJson("getinfo", false, resp) } } @@ -229,7 +231,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(status == OK) val resp = entityAs[String] assert(resp.contains(Alice.nodeParams.nodeId.toString)) - matchTestJson("close", false ,resp) + matchTestJson("close", false, resp) } } @@ -248,7 +250,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { })) )) - Post("/connect", FormData("nodeId" -> remoteNodeId, "host" -> remoteHost, "port" -> remotePort).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> @@ -272,7 +273,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { private def matchTestJson(apiName: String, overWrite: Boolean, response: String)(implicit formats: Formats) = { val p = Paths.get(s"src/test/resources/api/$apiName") - if(overWrite) { + if (overWrite) { Files.writeString(p, response) assert(false, "'overWrite' should be false before commit") } else { From 1e79bd18a763a9ce6c2d38f0a17bae3c0f6c363b Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 18:22:21 +0100 Subject: [PATCH 45/75] Timeout response not a json --- eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 93726fd43d..47299e4749 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -78,7 +78,7 @@ trait NewService extends Directives with Logging with MetaService { } val timeoutResponse: HttpRequest => HttpResponse = { r => - HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """) + HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, "request timed out") } def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { From 961bf3c1ddb08584c54deb209405b34fc1b6abed Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 18:23:09 +0100 Subject: [PATCH 46/75] Remove custom rejections --- .../fr/acinq/eclair/api/NewService.scala | 251 +++++++++--------- 1 file changed, 119 insertions(+), 132 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala index 47299e4749..d07642e10c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala @@ -46,13 +46,6 @@ trait NewService extends Directives with Logging with MetaService { complete(StatusCodes.InternalServerError, s"Error: $t") } - val apiRejectionHandler = RejectionHandler.newBuilder() - .handle { - case UnknownMethodRejection => complete(StatusCodes.BadRequest, "Wrong method or params combination") - case UnknownParamsRejection => complete(StatusCodes.BadRequest, "Wrong params combination") - } - .result() - val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: `Access-Control-Allow-Methods`(POST) :: `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil @@ -86,138 +79,132 @@ trait NewService extends Directives with Logging with MetaService { case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force } - case object UnknownMethodRejection extends Rejection - - case object UnknownParamsRejection extends Rejection - val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { - handleRejections(apiRejectionHandler) { - withRequestTimeoutResponse(timeoutResponse) { - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => - post { - path("getinfo") { - complete(eclairApi.getInfoResponse()) + withRequestTimeoutResponse(timeoutResponse) { + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => + post { + path("getinfo") { + complete(eclairApi.getInfoResponse()) + } ~ + path("help") { + complete(help) + } ~ + path("connect") { + formFields("uri".as[String]) { uri => + complete(eclairApi.connect(uri)) + } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => + complete(eclairApi.connect(s"$nodeId@$host:$port")) + } + } ~ + path("open") { + formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) + } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => + complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) + } + } ~ + path("forceclose") { + formFields(channelIdNamedParameter) { channelId => + complete(eclairApi.forceClose(Left(channelId))) + } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => + complete(eclairApi.forceClose(Right(shortChannelId))) + } + } ~ + path("updaterelayfee") { + formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(eclairApi.peersInfo()) + } ~ + path("channels") { + formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + formFields(channelIdNamedParameter) { channelId => + complete(eclairApi.channelInfo(channelId)) + } + } ~ + path("allnodes") { + complete(eclairApi.allnodes()) + } ~ + path("allchannels") { + complete(eclairApi.allchannels()) + } ~ + path("allupdates") { + formFields("nodeId".as[PublicKey].?) { nodeId_opt => + complete(eclairApi.allupdates(nodeId_opt)) + } + } ~ + path("receive") { + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(eclairApi.receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + formFields("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } } ~ - path("help") { - complete(help) - } ~ - path("connect") { - formFields("uri".as[String]) { uri => - complete(eclairApi.connect(uri)) - } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => - complete(eclairApi.connect(s"$nodeId@$host:$port")) - } - } ~ - path("open") { - formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) - } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => - complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) - } - } ~ - path("forceclose") { - formFields(channelIdNamedParameter) { channelId => - complete(eclairApi.forceClose(Left(channelId))) - } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => - complete(eclairApi.forceClose(Right(shortChannelId))) - } - } ~ - path("updaterelayfee") { - formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(eclairApi.peersInfo()) - } ~ - path("channels") { - formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - formFields(channelIdNamedParameter) { channelId => - complete(eclairApi.channelInfo(channelId)) - } - } ~ - path("allnodes") { - complete(eclairApi.allnodes()) - } ~ - path("allchannels") { - complete(eclairApi.allchannels()) - } ~ - path("allupdates") { - formFields("nodeId".as[PublicKey].?) { nodeId_opt => - complete(eclairApi.allupdates(nodeId_opt)) - } - } ~ - path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(eclairApi.receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - formFields("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findRouteByInvoice") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) - case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) - } - } ~ path("findRouteByNode") { - formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) + path("findRouteByInvoice") { + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) + } + } ~ path("findRouteByNode") { + formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ + path("sendToInvoice") { + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => + complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + case (invoice, Some(overrideAmount)) => + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendToNode") { + formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + complete(eclairApi.send(nodeId, amountMsat, paymentHash)) + } + } ~ + path("checkpayment") { + formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => + complete(eclairApi.checkpayment(paymentHash)) + } ~ formFields("invoice".as[PaymentRequest]) { invoice => + complete(eclairApi.checkpayment(invoice.paymentHash)) } } ~ - path("sendToInvoice") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => - complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case (invoice, Some(overrideAmount)) => - complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) - } - } ~ - path("sendToNode") { - formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => - complete(eclairApi.send(nodeId, amountMsat, paymentHash)) - } - } ~ - path("checkpayment") { - formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => - complete(eclairApi.checkpayment(paymentHash)) - } ~ formFields("invoice".as[PaymentRequest]) { invoice => - complete(eclairApi.checkpayment(invoice.paymentHash)) - } - } ~ - path("audit") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.audit(from, to)) - } - } ~ - path("networkfees") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.networkFees(from, to)) - } - } ~ - path("channelstats") { - complete(eclairApi.channelStats()) - } ~ - path("ws") { - handleWebSocketMessages(makeSocketHandler) + path("audit") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(eclairApi.audit(from, to)) } - } + } ~ + path("networkfees") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(eclairApi.networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(eclairApi.channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) + } } } } From 56f910cf5bb98c5dd23e73a0083c667cd3e00882 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 18 Mar 2019 10:03:08 +0100 Subject: [PATCH 47/75] Formatting commands in eclair-cli --- eclair-core/eclair-cli | 70 +++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index ebecd7ee48..f3d16f918b 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -67,65 +67,65 @@ shift 1 # Whatever the arguments provided to eclair-cli, a call to the API will be sent. Let it fail! case ${METHOD}_${#} in - ""_*) displayhelp ;; - "help"*) displayhelp - echo -e "\nAvailable commands:\n" - call "help" "" ;; + ""_*) displayhelp ;; + "help"*) displayhelp + echo -e "\nAvailable commands:\n" + call "help" "" ;; - "getinfo_0") call ${METHOD} "" ;; + "getinfo_0") call ${METHOD} "" ;; - "connect_1") call ${METHOD} "$(printf uri=%s ${1})" ;; - "connect_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf host=%s ${2})" "$(printf port=%s ${3})" " ;; + "connect_1") call ${METHOD} "$(printf uri=%s ${1})" ;; + "connect_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf host=%s ${2})" "$(printf port=%s ${3})" " ;; - "open_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" " ;; - "open_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" " ;; - "open_4") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" " ;; - "open_5") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" "$(printf fundingFeerateSatByte=%s ${5})" " ;; + "open_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" " ;; + "open_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" " ;; + "open_4") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" " ;; + "open_5") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" "$(printf fundingFeerateSatByte=%s ${5})" " ;; - "close_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; - "close_2") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf scriptPubKey=%s ${2})" " ;; + "close_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; + "close_2") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf scriptPubKey=%s ${2})" " ;; - "forceclose_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; + "forceclose_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; - "updaterelayfee_3") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf feeBaseMsat=%s ${2})" "$(printf feeProportionalMillionths=%s ${3})" " ;; + "updaterelayfee_3") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf feeBaseMsat=%s ${2})" "$(printf feeProportionalMillionths=%s ${3})" " ;; - "peers_0") call ${METHOD} "" ;; + "peers_0") call ${METHOD} "" ;; - "channel_1") call ${METHOD} "$(printf channelId=%s ${1})" ". | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }" ;; + "channel_1") call ${METHOD} "$(printf channelId=%s ${1})" ". | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }" ;; - "channels_0") call ${METHOD} "" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - "channels_1") call ${METHOD} "$(printf toRemoteNodeId=%s ${1})" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; + "channels_0") call ${METHOD} "" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; + "channels_1") call ${METHOD} "$(printf toRemoteNodeId=%s ${1})" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - "allnodes_0") call ${METHOD} "" ;; + "allnodes_0") call ${METHOD} "" ;; - "allchannels_0") call ${METHOD} "" ;; + "allchannels_0") call ${METHOD} "" ;; - "allupdates_0") call ${METHOD} "" ;; - "allupdates_1") call ${METHOD} "$(printf nodeId=%s ${1})" ;; + "allupdates_0") call ${METHOD} "" ;; + "allupdates_1") call ${METHOD} "$(printf nodeId=%s ${1})" ;; - "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; - "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; + "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; + "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; - "sendToInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "sendToInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "sendToNode_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; + "sendToInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "sendToInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "sendToNode_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; - "parseinvoice_1") call ${METHOD} "$(printf invoice=%s ${1})" ;; + "parseinvoice_1") call ${METHOD} "$(printf invoice=%s ${1})" ;; "findRouteByInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; "findRouteByInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; "findRouteByNode_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "checkpayment_1") call "checkpayment" "$(printf invoice=%s ${1})" ;; + "checkpayment_1") call "checkpayment" "$(printf invoice=%s ${1})" ;; "checkpaymentbyhash_1") call "checkpayment" "$(printf paymentHash=%s ${1})" ;; # calls checkinvoice but using the paymentHash instead of the invoice - "audit_0") call ${METHOD} "" ;; - "audit_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; + "audit_0") call ${METHOD} "" ;; + "audit_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; - "networkfees_0") call ${METHOD} "" ;; - "networkfees_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; + "networkfees_0") call ${METHOD} "" ;; + "networkfees_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; - "channelstats_0") call ${METHOD} "" ;; + "channelstats_0") call ${METHOD} "" ;; *) displayhelp ; exit 1 ;; # Default case. From db15e2bb014a73a1c75167deae893977914ca3e1 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 19 Mar 2019 11:40:10 +0100 Subject: [PATCH 48/75] Revert "Merge remote-tracking branch 'origin/use-bitcoin-0.17.1' into api_revamp" This reverts commit b52ba9c05e6aec92a0441b55397ecf89626abf28, reversing changes made to 837ac03dd9cb85d02b56b3a730653e1c2530c792. --- eclair-core/pom.xml | 18 +++--- .../main/scala/fr/acinq/eclair/Setup.scala | 4 +- .../bitcoind/BitcoinCoreWallet.scala | 14 ++--- .../test/resources/integration/bitcoin.conf | 4 +- .../bitcoind/BitcoinCoreWalletSpec.scala | 58 +++---------------- .../blockchain/bitcoind/BitcoindService.scala | 2 +- .../bitcoind/ExtendedBitcoinClientSpec.scala | 13 +---- .../fee/BitcoinCoreFeeProviderSpec.scala | 9 +-- .../transactions/TransactionsSpec.scala | 2 +- travis/builddeps.sh | 25 ++++++++ 10 files changed, 57 insertions(+), 92 deletions(-) create mode 100755 travis/builddeps.sh diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 22d61f6c7a..679f4a5915 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -79,10 +79,10 @@ true - https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-x86_64-linux-gnu.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz - 724043999e2b5ed0c088e8db34f15d43 - 546ee35d4089c7ccc040a01cdff3362599b8bc53 + c371e383f024c6c45fb255d528a6beec + e6d8ab1f7661a6654fd81e236b9b5fd35a3d4dcb @@ -93,10 +93,10 @@ - https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-osx64.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-osx64.tar.gz - b5a792c6142995faa42b768273a493bd - 8bd51c7024d71de07df381055993e9f472013db8 + bacd87d0c3f65a5acd666e33d094a59e + 62cc5bd9ced610bb9e8d4a854396bfe2139e3d0f @@ -107,9 +107,9 @@ - https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-win64.zip - b0e824e9dd02580b5b01f073f3c89858 - 4e17bad7d08c465b444143a93cd6eb1c95076e3f + https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-win64.zip + bbde9b1206956d19298034319e9f405e + 85e3dc8a9c6f93b1b20cb79fa5850b5ce81da221 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index b9c6d731db..fda8f0d248 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -126,13 +126,15 @@ class Setup(datadir: File, } yield (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) // blocking sanity checks val (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") - assert(bitcoinVersion >= 170000, "Eclair requires Bitcoin Core 0.17.0 or higher") + assert(bitcoinVersion >= 160300, "Eclair requires Bitcoin Core 0.16.3 or higher") assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") if (chainHash != Block.RegtestGenesisBlock.hash) { assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Make sure that all your UTXOS are segwit UTXOS and not p2pkh (check out our README for more details)") } assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress") assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks") + // TODO: add a check on bitcoin version? + Bitcoind(bitcoinClient) case ELECTRUM => val addresses = config.hasPath("electrum") match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 008326fa00..9a7f3bc4d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -19,12 +19,10 @@ package fr.acinq.eclair.blockchain.bitcoind import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError} import fr.acinq.eclair.transactions.Transactions import grizzled.slf4j.Logging -import org.json4s.DefaultFormats import org.json4s.JsonAST._ -import org.json4s.jackson.Serialization import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} @@ -49,13 +47,9 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw) def signTransaction(hex: String): Future[SignTransactionResponse] = - rpcClient.invoke("signrawtransactionwithwallet", hex).map(json => { + rpcClient.invoke("signrawtransaction", hex).map(json => { val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" - if (!complete) { - val message = (json \ "errors" \\ classOf[JString]).mkString(",") - throw new JsonRPCError(Error(-1, message)) - } SignTransactionResponse(Transaction.read(hex), complete) }) @@ -80,7 +74,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = { val f = signTransaction(tx) - // if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos + // if signature fails (e.g. because wallet is uncrypted) we need to unlock the utxos f.recoverWith { case _ => unlockOutpoints(tx.txIn.map(_.outPoint)) .recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure @@ -100,7 +94,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC // we ask bitcoin core to add inputs to the funding tx, and use the specified change address FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw) // now let's sign the funding tx - SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx) + SignTransactionResponse(fundingTx, _) <- signTransactionOrUnlock(unsignedFundingTx) // there will probably be a change output, so we need to find which output is ours outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee") diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index 29775744a1..da4dd59a07 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -1,7 +1,7 @@ regtest=1 -noprinttoconsole=1 server=1 port=28333 +rpcport=28332 rpcuser=foo rpcpassword=bar txindex=1 @@ -9,5 +9,3 @@ zmqpubrawblock=tcp://127.0.0.1:28334 zmqpubrawtx=tcp://127.0.0.1:28335 rpcworkqueue=64 addresstype=bech32 -[regtest] -rpcport=28332 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 3d0a510292..2f4adea65d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,7 +21,7 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{ByteVector32, Block, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script, Transaction} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} @@ -41,14 +41,7 @@ import scala.util.{Random, Try} class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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.router-broadcast-interval" -> "2 second", - "eclair.auto-reconnect" -> false)) + 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 walletPassword = Random.alphanumeric.take(8).mkString @@ -101,39 +94,6 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe } } - test("handle errors when signing transactions") { - val bitcoinClient = new BasicBitcoinJsonRPCClient( - 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() - - // create a transaction that spends UTXOs that don't exist - wallet.getFinalAddress.pipeTo(sender.ref) - val address = sender.expectMsgType[String] - val unknownTxids = Seq( - ByteVector32.fromValidHex("01" * 32), - ByteVector32.fromValidHex("02" * 32), - ByteVector32.fromValidHex("03" * 32) - ) - val unsignedTx = Transaction(version = 2, - txIn = Seq( - TxIn(OutPoint(unknownTxids(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), - TxIn(OutPoint(unknownTxids(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), - TxIn(OutPoint(unknownTxids(2), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) - ), - txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, - lockTime = 0) - - // signing it should fail, and the error message should contain the txids of the UTXOs that could not be used - wallet.signTransaction(unsignedTx).pipeTo(sender.ref) - val Failure(JsonRPCError(error)) = sender.expectMsgType[Failure] - unknownTxids.foreach(id => assert(error.message.contains(id.toString()))) - } - test("create/commit/rollback funding txes") { val bitcoinClient = new BasicBitcoinJsonRPCClient( user = config.getString("bitcoind.rpcuser"), @@ -181,9 +141,10 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString())) assert(sender.expectMsgType[JString](10 seconds).s === fundingTxes(2).toString()) - // NB: from 0.17.0 on bitcoin core will clear locks when a tx is published + // 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 === 0) + assert(sender.expectMsgType[JValue](10 seconds).children.size === 2) + } test("encrypt wallet") { @@ -216,8 +177,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref) - val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error - assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first")) + assert(sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error.message.contains("Please enter the wallet passphrase with walletpassphrase first.")) sender.send(bitcoincli, BitcoinReq("listlockunspent")) assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) @@ -252,14 +212,14 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe bitcoinClient.invoke("fundrawtransaction", noinputTx1).pipeTo(sender.ref) val json = sender.expectMsgType[JValue] val JString(unsignedtx1) = json \ "hex" - bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx1).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransaction", unsignedtx1).pipeTo(sender.ref) val JString(signedTx1) = sender.expectMsgType[JValue] \ "hex" val tx1 = Transaction.read(signedTx1) // let's then generate another tx that double spends the first one val inputs = tx1.txIn.map(txIn => Map("txid" -> txIn.outPoint.txid.toString, "vout" -> txIn.outPoint.index)).toArray bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount.toLong).sum * 1.0 / 1e8)).pipeTo(sender.ref) val JString(unsignedtx2) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransaction", unsignedtx2).pipeTo(sender.ref) val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" val tx2 = Transaction.read(signedTx2) @@ -276,4 +236,4 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.expectMsg(true) } -} +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 479072f4e9..42f7f949c3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -44,7 +44,7 @@ trait BitcoindService extends Logging { val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}") logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.17.1/bin/bitcoind") + val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.16.3/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index 18a08923de..d730b2bf13 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -34,14 +34,7 @@ import scala.concurrent.ExecutionContext.Implicits.global class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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.router-broadcast-interval" -> "2 second", - "eclair.auto-reconnect" -> false)) + 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") implicit val formats = DefaultFormats @@ -74,7 +67,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val json = sender.expectMsgType[JValue] val JString(unsignedtx) = json \ "hex" val JInt(changePos) = json \ "changepos" - bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" val tx = Transaction.read(signedTx) val txid = tx.txid.toString() @@ -99,7 +92,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val pos = if (changePos == 0) 1 else 0 bitcoinClient.invoke("createrawtransaction", Array(Map("txid" -> txid, "vout" -> pos)), Map(address -> 5.99999)).pipeTo(sender.ref) val JString(unsignedtx) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" signedTx } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 7b9126b2ab..82c292166c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -38,14 +38,7 @@ import scala.util.Random class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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.router-broadcast-interval" -> "2 second", - "eclair.auto-reconnect" -> false)) + 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 walletPassword = Random.alphanumeric.take(8).mkString diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 8e32ae7e12..d81b2c8d1a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -215,7 +215,7 @@ class TransactionsSpec extends FunSuite with Logging { assert(getCommitTxNumber(commitTx.tx, true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) val hash = Crypto.sha256(localPaymentPriv.publicKey.toBin ++ remotePaymentPriv.publicKey.toBin) val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL - val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.tx.lockTime & 0xffffff) + val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | commitTx.tx.lockTime assert((check ^ num) == commitTxNumber) } val (htlcTimeoutTxs, htlcSuccessTxs) = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) diff --git a/travis/builddeps.sh b/travis/builddeps.sh new file mode 100755 index 0000000000..bfff183d5f --- /dev/null +++ b/travis/builddeps.sh @@ -0,0 +1,25 @@ +pushd . +# lightning deps +sudo add-apt-repository -y ppa:chris-lea/libsodium +sudo apt-get update +sudo apt-get install -y libsodium-dev libgmp-dev libsqlite3-dev +cd +git clone https://github.com/luke-jr/libbase58.git +cd libbase58 +./autogen.sh && ./configure && make && sudo make install +# lightning +cd +git clone https://github.com/ElementsProject/lightning.git +cd lightning +git checkout fce9ee29e3c37b4291ebb050e6a687cfaa7df95a +git submodule init +git submodule update +make +# bitcoind +cd +wget https://bitcoin.org/bin/bitcoin-core-0.13.0/bitcoin-0.13.0-x86_64-linux-gnu.tar.gz +echo "bcc1e42d61f88621301bbb00512376287f9df4568255f8b98bc10547dced96c8 bitcoin-0.13.0-x86_64-linux-gnu.tar.gz" > sha256sum.asc +sha256sum -c sha256sum.asc +tar xzvf bitcoin-0.13.0-x86_64-linux-gnu.tar.gz +popd + From bd02e8ccfed2f21ba851b69ff5db024bd7827f23 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 19 Mar 2019 13:17:16 +0100 Subject: [PATCH 49/75] Add comment to MetaService --- eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala index ee36d035dc..d3b820aefd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala @@ -2,6 +2,7 @@ package fr.acinq.eclair.api import akka.http.scaladsl.server.Route +// TODO remove this as soon as we remove Service.scala trait MetaService { val route: Route From f07741cfbfa66eb7d6a48256c4442fbb07fcc759 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 10:17:35 +0100 Subject: [PATCH 50/75] Renaming Service -> OldService, NewService -> Service --- .../main/scala/fr/acinq/eclair/Setup.scala | 4 +- .../fr/acinq/eclair/api/NewService.scala | 251 --------- .../fr/acinq/eclair/api/OldService.scala | 422 ++++++++++++++ .../scala/fr/acinq/eclair/api/Service.scala | 528 ++++++------------ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- 5 files changed, 603 insertions(+), 606 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index fda8f0d248..e4a309dfd5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -272,7 +272,7 @@ class Setup(datadir: File, publicAddresses = nodeParams.publicAddresses) val api = if (!config.getBoolean("api.use-old-api")) { - new NewService { + new Service { override val actorSystem = kit.system @@ -287,7 +287,7 @@ class Setup(datadir: File, } } else { - new Service { + new OldService { override def scheduler = system.scheduler diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala deleted file mode 100644 index d07642e10c..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/NewService.scala +++ /dev/null @@ -1,251 +0,0 @@ -package fr.acinq.eclair.api - -import akka.http.scaladsl.server._ -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} -import fr.acinq.eclair.{Kit, ShortChannelId} -import FormParamExtractors._ -import akka.NotUsed -import akka.actor.{Actor, ActorRef, ActorSystem, Props} -import akka.http.scaladsl.model.HttpMethods.POST -import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes} -import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} -import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} -import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} -import akka.stream.{ActorMaterializer, OverflowStrategy} -import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} -import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} -import grizzled.slf4j.Logging -import scodec.bits.ByteVector -import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.duration._ - -trait NewService extends Directives with Logging with MetaService { - - // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 - import JsonSupport.marshaller - import JsonSupport.formats - import JsonSupport.serialization - - def password: String - - def eclairApi: EclairApi - - implicit val actorSystem: ActorSystem - implicit lazy val ec = actorSystem.dispatcher - implicit val mat: ActorMaterializer - - // a named and typed URL parameter used across several routes, 32-bytes hex-encoded - val channelIdNamedParameter = "channelId".as[ByteVector32](sha256HashUnmarshaller) - val shortChannelIdNamedParameter = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) - - val apiExceptionHandler = ExceptionHandler { - case t: Throwable => - logger.error(s"API call failed with cause=${t.getMessage}", t) - complete(StatusCodes.InternalServerError, s"Error: $t") - } - - val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: - `Access-Control-Allow-Methods`(POST) :: - `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil - - lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { - - // create a flow transforming a queue of string -> string - val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() - - // register an actor that feeds the queue when a payment is received - actorSystem.actorOf(Props(new Actor { - override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) - - def receive: Receive = { - case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) - } - })) - - Flow[Message] - .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client - .merge(flowOutput) // Stream the data we want to the client - .map(TextMessage.apply) - } - - val timeoutResponse: HttpRequest => HttpResponse = { r => - HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, "request timed out") - } - - def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { - case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) - case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force - } - - val route: Route = { - respondWithDefaultHeaders(customHeaders) { - handleExceptions(apiExceptionHandler) { - withRequestTimeoutResponse(timeoutResponse) { - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => - post { - path("getinfo") { - complete(eclairApi.getInfoResponse()) - } ~ - path("help") { - complete(help) - } ~ - path("connect") { - formFields("uri".as[String]) { uri => - complete(eclairApi.connect(uri)) - } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => - complete(eclairApi.connect(s"$nodeId@$host:$port")) - } - } ~ - path("open") { - formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) - } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => - complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) - } - } ~ - path("forceclose") { - formFields(channelIdNamedParameter) { channelId => - complete(eclairApi.forceClose(Left(channelId))) - } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => - complete(eclairApi.forceClose(Right(shortChannelId))) - } - } ~ - path("updaterelayfee") { - formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(eclairApi.peersInfo()) - } ~ - path("channels") { - formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - formFields(channelIdNamedParameter) { channelId => - complete(eclairApi.channelInfo(channelId)) - } - } ~ - path("allnodes") { - complete(eclairApi.allnodes()) - } ~ - path("allchannels") { - complete(eclairApi.allchannels()) - } ~ - path("allupdates") { - formFields("nodeId".as[PublicKey].?) { nodeId_opt => - complete(eclairApi.allupdates(nodeId_opt)) - } - } ~ - path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(eclairApi.receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - formFields("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findRouteByInvoice") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) - case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) - } - } ~ path("findRouteByNode") { - formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) - } - } ~ - path("sendToInvoice") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => - complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case (invoice, Some(overrideAmount)) => - complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) - } - } ~ - path("sendToNode") { - formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => - complete(eclairApi.send(nodeId, amountMsat, paymentHash)) - } - } ~ - path("checkpayment") { - formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => - complete(eclairApi.checkpayment(paymentHash)) - } ~ formFields("invoice".as[PaymentRequest]) { invoice => - complete(eclairApi.checkpayment(invoice.paymentHash)) - } - } ~ - path("audit") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.audit(from, to)) - } - } ~ - path("networkfees") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.networkFees(from, to)) - } - } ~ - path("channelstats") { - complete(eclairApi.channelStats()) - } ~ - path("ws") { - handleWebSocketMessages(makeSocketHandler) - } - } - } - } - } - } - } - - val help = List( - "connect (uri): open a secure connection to a lightning node", - "connect (nodeId, host, port): open a secure connection to a lightning node", - "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", - "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", - "peers: list existing local peers", - "channels: list existing local channels", - "channels (nodeId): list existing local channels to a particular nodeId", - "channel (channelId): retrieve detailed information about a given channel", - "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", - "allnodes: list all known nodes", - "allchannels: list all known channels", - "allupdates: list all channels updates", - "allupdates (nodeId): list all channels updates for this nodeId", - "receive (amountMsat, description): generate a payment request for a given amount", - "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", - "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", - "findroute (paymentRequest): returns nodes and channels of the route if there is any", - "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", - "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", - "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", - "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", - "close (channelId): close a channel", - "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", - "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", - "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", - "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", - "audit: list all send/received/relayed payments", - "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", - "networkfees: list all network fees paid to the miners, by transaction", - "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", - "getinfo: returns info about the blockchain and this node", - "help: display this message") - - -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala new file mode 100644 index 0000000000..f02da14df0 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -0,0 +1,422 @@ +/* + * Copyright 2018 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.api + +import akka.NotUsed +import akka.actor.{Actor, ActorRef, ActorSystem, Props, Scheduler} +import akka.http.scaladsl.model.HttpMethods._ +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.model.ws.{Message, TextMessage} +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server._ +import akka.http.scaladsl.server.directives.Credentials +import akka.http.scaladsl.server.directives.RouteDirectives.reject +import akka.pattern.ask +import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} +import akka.stream.{ActorMaterializer, OverflowStrategy} +import akka.util.Timeout +import de.heikoseeberger.akkahttpjson4s.Json4sSupport +import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} +import fr.acinq.eclair.io.{NodeURI, Peer} +import fr.acinq.eclair.payment.PaymentLifecycle._ +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} +import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw} +import grizzled.slf4j.Logging +import org.json4s.JsonAST.{JBool, JInt, JString} +import org.json4s.{JValue, jackson} +import scodec.bits.ByteVector + +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} + +// @formatter:off +case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "eclair-node", method: String, params: Seq[JValue]) +case class Error(code: Int, message: String) +case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String) +case class Status(node_id: String) +case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) +case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) +trait RPCRejection extends Rejection { + def requestId: String +} +final case class UnknownMethodRejection(requestId: String) extends RPCRejection +final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection +final case class RpcValidationRejection(requestId: String, message: String) extends RPCRejection +final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection +// @formatter:on + +trait OldService extends Logging with MetaService { + + implicit def ec: ExecutionContext = ExecutionContext.Implicits.global + + def scheduler: Scheduler + + implicit val serialization = jackson.Serialization + implicit val formats = org.json4s.DefaultFormats + new ByteVectorSerializer + new ByteVector32Serializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer +new PaymentRequestSerializer + implicit val timeout = Timeout(60 seconds) + implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True + + import Json4sSupport.{marshaller, unmarshaller} + + def password: String + + def appKit: Kit + + val socketHandler: Flow[Message, TextMessage.Strict, NotUsed] + + def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { + case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) + case _ => akka.pattern.after(1 second, using = scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force + } + + val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: + `Access-Control-Allow-Methods`(POST) :: + `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil + + val myExceptionHandler = ExceptionHandler { + case t: Throwable => + extractRequest { _ => + logger.error(s"API call failed with cause=${t.getMessage}") + complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), "-1")) + } + } + + def completeRpcFuture(requestId: String, future: Future[AnyRef]): Route = onComplete(future) { + case Success(s) => completeRpc(requestId, s) + case Failure(t) => reject(ExceptionRejection(requestId, t.getLocalizedMessage)) + } + + def completeRpc(requestId: String, result: AnyRef): Route = complete(JsonRPCRes(result, None, requestId)) + + val myRejectionHandler: RejectionHandler = RejectionHandler.newBuilder() + .handleNotFound { + complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), "-1")) + } + .handle { + case _: AuthenticationFailedRejection ⇒ complete(StatusCodes.Unauthorized, JsonRPCRes(null, Some(Error(StatusCodes.Unauthorized.intValue, "Access restricted")), "-1")) + case v: RpcValidationRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId)) + case ukm: UnknownMethodRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, "method not found")), ukm.requestId)) + case p: UnknownParamsRejection ⇒ complete(StatusCodes.BadRequest, + JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"invalid parameters for this method, should be: ${p.message}")), p.requestId)) + case m: MalformedRequestContentRejection ⇒ complete(StatusCodes.BadRequest, + JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"malformed parameters for this method: ${m.message}")), "-1")) + case e: ExceptionRejection ⇒ complete(StatusCodes.BadRequest, + JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"command failed: ${e.message}")), e.requestId)) + case r ⇒ logger.error(s"API call failed with cause=$r") + complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1")) + } + .result() + + val route: Route = + respondWithDefaultHeaders(customHeaders) { + withRequestTimeoutResponse(r => HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """)) { + handleExceptions(myExceptionHandler) { + handleRejections(myRejectionHandler) { + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => + pathSingleSlash { + post { + entity(as[JsonRPCBody]) { + req => + val kit = appKit + import kit._ + + req.method match { + // utility methods + case "getinfo" => completeRpcFuture(req.id, getInfoResponse) + case "help" => completeRpc(req.id, help) + + // channel lifecycle methods + case "connect" => req.params match { + case JString(pubkey) :: JString(host) :: JInt(port) :: Nil => + completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String]) + case JString(uri) :: Nil => + completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) + case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]")) + } + case "open" => req.params match { + case JString(nodeId) :: JInt(fundingSatoshis) :: Nil => + completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(0), fundingTxFeeratePerKw_opt = None, channelFlags = None)).mapTo[String]) + case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: Nil => + completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None, fundingTxFeeratePerKw_opt = None)).mapTo[String]) + case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: JInt(fundingFeerateSatPerByte) :: Nil => + completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = None)).mapTo[String]) + case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: JInt(fundingFeerateSatPerByte) :: JInt(flags) :: Nil => + completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = Some(flags.toByte))).mapTo[String]) + case _ => reject(UnknownParamsRejection(req.id, s"[nodeId, fundingSatoshis], [nodeId, fundingSatoshis, pushMsat], [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte] or [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte, flag]")) + } + case "close" => req.params match { + case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String]) + case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(ByteVector.fromValidHex(scriptPubKey)))).mapTo[String]) + case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]")) + } + case "forceclose" => req.params match { + case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_FORCECLOSE).mapTo[String]) + case _ => reject(UnknownParamsRejection(req.id, "[channelId]")) + } + case "updaterelayfee" => req.params match { + case JString(identifier) :: JInt(feeBaseMsat) :: JInt(feeProportionalMillionths) :: Nil => + completeRpcFuture(req.id, sendToChannel(identifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat.toLong, feeProportionalMillionths.toLong)).mapTo[String]) + case JString(identifier) :: JString(feeBaseMsat) :: JString(feeProportionalMillionths) :: Nil => + completeRpcFuture(req.id, sendToChannel(identifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat.toLong, feeProportionalMillionths.toLong)).mapTo[String]) + case _ => reject(UnknownParamsRejection(req.id, "[channelId] [feeBaseMsat] [feeProportionalMillionths]")) + } + // local network methods + case "peers" => completeRpcFuture(req.id, for { + peers <- (switchboard ? 'peers).mapTo[Iterable[ActorRef]] + peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) + } yield peerinfos) + case "channels" => req.params match { + case Nil => + val f = for { + channels_id <- (register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys) + channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + } yield channels + completeRpcFuture(req.id, f) + case JString(remoteNodeId) :: Nil => Try(PublicKey(ByteVector.fromValidHex(remoteNodeId))) match { + case Success(pk) => + val f = for { + channels_id <- (register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys) + channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) + } yield channels + completeRpcFuture(req.id, f) + case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'")) + } + case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]")) + } + case "channel" => req.params match { + case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO]) + case _ => reject(UnknownParamsRejection(req.id, "[channelId]")) + } + + // global network methods + case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]]) + case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)))) + case "allupdates" => req.params match { + case JString(nodeId) :: Nil => Try(PublicKey(ByteVector.fromValidHex(nodeId))) match { + case Success(pk) => completeRpcFuture(req.id, (router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values)) + case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$nodeId'")) + } + case _ => completeRpcFuture(req.id, (router ? 'updates).mapTo[Iterable[ChannelUpdate]]) + } + + // payment methods + case "receive" => req.params match { + // only the payment description is given: user may want to generate a donation payment request + case JString(description) :: Nil => + completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write)) + // the amount is now given with the description + case JInt(amountMsat) :: JString(description) :: Nil => + completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write)) + case JInt(amountMsat) :: JString(description) :: JInt(expirySeconds) :: Nil => + completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description, Some(expirySeconds.toLong))).mapTo[PaymentRequest].map(PaymentRequest.write)) + case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description] or [amount, description, expiryDuration]")) + } + + // checkinvoice deprecated. + case "parseinvoice" | "checkinvoice" => req.params match { + case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match { + case Success(pr) => completeRpc(req.id,pr) + case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getMessage}")) + } + case _ => reject(UnknownParamsRejection(req.id, "[payment_request]")) + } + + case "findroute" => req.params match { + case JString(nodeId) :: JInt(amountMsat) :: Nil if nodeId.length() == 66 => Try(PublicKey(ByteVector.fromValidHex(nodeId))) match { + case Success(pk) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, pk, amountMsat.toLong)).mapTo[RouteResponse]) + case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid nodeId hash '$nodeId'")) + } + case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match { + case Success(PaymentRequest(_, Some(amountMsat), _, nodeId , _, _)) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat.toLong)).mapTo[RouteResponse]) + case Success(_) => reject(RpcValidationRejection(req.id, s"payment request is missing amount, please specify it")) + case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getLocalizedMessage}")) + } + case JString(paymentRequest) :: JInt(amountMsat) :: Nil => Try(PaymentRequest.read(paymentRequest)) match { + case Success(PaymentRequest(_, None, _, nodeId , _, _)) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat.toLong)).mapTo[RouteResponse]) + case Success(_) => reject(RpcValidationRejection(req.id, s"amount was specified both in payment request and api call")) + case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getLocalizedMessage}")) + } + case _ => reject(UnknownParamsRejection(req.id, "[payment_request] or [payment_request, amountMsat] or [nodeId, amountMsat]")) + } + + case "send" => req.params match { + // user manually sets the payment information + case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil => + (Try(ByteVector32.fromValidHex(paymentHash)), Try(PublicKey(ByteVector.fromValidHex(nodeId)))) match { + case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ? + SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult].map { + case s: PaymentSucceeded => s + case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) + }) + case (Failure(_), _) => reject(RpcValidationRejection(req.id, s"invalid payment hash '$paymentHash'")) + case _ => reject(RpcValidationRejection(req.id, s"invalid node id '$nodeId'")) + } + // user gives a Lightning payment request + case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match { + case Success(pr) => + // setting the payment amount + val amount_msat: Long = (pr.amount, rest) match { + // optional amount always overrides the amount in the payment request + case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong + case (Some(amount_msat_pr), _) => amount_msat_pr.amount + case _ => throw new RuntimeException("you must manually specify an amount for this payment request") + } + logger.debug(s"api call for sending payment with amount_msat=$amount_msat") + // optional cltv expiry + val sendPayment = pr.minFinalCltvExpiry match { + case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId) + case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry) + } + completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult].map { + case s: PaymentSucceeded => s + case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) + }) + case _ => reject(RpcValidationRejection(req.id, s"payment request is not valid")) + } + case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]")) + } + + // check received payments + case "checkpayment" => req.params match { + case JString(identifier) :: Nil => completeRpcFuture(req.id, for { + paymentHash <- Try(PaymentRequest.read(identifier)) match { + case Success(pr) => Future.successful(pr.paymentHash) + case _ => Try(ByteVector.fromValidHex(identifier)) match { + case Success(s) => Future.successful(s) + case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash")) + } + } + found <- (paymentHandler ? CheckPayment(ByteVector32.fromValidHex(identifier))).map(found => new JBool(found.asInstanceOf[Boolean])) + } yield found) + case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]")) + } + + // retrieve audit events + case "audit" => + val (from, to) = req.params match { + case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong) + case _ => (0L, Long.MaxValue) + } + completeRpcFuture(req.id, Future(AuditResponse( + sent = nodeParams.auditDb.listSent(from, to), + received = nodeParams.auditDb.listReceived(from, to), + relayed = nodeParams.auditDb.listRelayed(from, to)) + )) + + case "networkfees" => + val (from, to) = req.params match { + case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong) + case _ => (0L, Long.MaxValue) + } + completeRpcFuture(req.id, Future(nodeParams.auditDb.listNetworkFees(from, to))) + + // retrieve fee stats + case "channelstats" => completeRpcFuture(req.id, Future(nodeParams.auditDb.stats)) + + + // method name was not found + case _ => reject(UnknownMethodRejection(req.id)) + } + } + } + } + } ~ path("ws") { + handleWebSocketMessages(socketHandler) + } + } + } + } + } + + def getInfoResponse: Future[GetInfoResponse] + + def makeSocketHandler(system: ActorSystem)(implicit materializer: ActorMaterializer): Flow[Message, TextMessage.Strict, NotUsed] = { + + // create a flow transforming a queue of string -> string + val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() + + // register an actor that feeds the queue when a payment is received + system.actorOf(Props(new Actor { + override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) + def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) } + })) + + Flow[Message] + .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client + .merge(flowOutput) // Stream the data we want to the client + .map(TextMessage.apply) + } + + def help = List( + "connect (uri): open a secure connection to a lightning node", + "connect (nodeId, host, port): open a secure connection to a lightning node", + "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", + "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", + "peers: list existing local peers", + "channels: list existing local channels", + "channels (nodeId): list existing local channels to a particular nodeId", + "channel (channelId): retrieve detailed information about a given channel", + "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", + "allnodes: list all known nodes", + "allchannels: list all known channels", + "allupdates: list all channels updates", + "allupdates (nodeId): list all channels updates for this nodeId", + "receive (amountMsat, description): generate a payment request for a given amount", + "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", + "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", + "findroute (paymentRequest): returns nodes and channels of the route if there is any", + "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", + "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", + "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", + "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", + "close (channelId): close a channel", + "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", + "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", + "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", + "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", + "audit: list all send/received/relayed payments", + "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", + "networkfees: list all network fees paid to the miners, by transaction", + "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", + "getinfo: returns info about the blockchain and this node", + "help: display this message") + + /** + * Sends a request to a channel and expects a response + * + * @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) + * @param request + * @return + */ + def sendToChannel(channelIdentifier: String, request: Any): Future[Any] = + for { + fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request)) + .recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) } + .recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) } + res <- appKit.register ? fwdReq + } yield res +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index b0f7574a54..6793dafed4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -1,376 +1,217 @@ -/* - * Copyright 2018 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package fr.acinq.eclair.api +import akka.http.scaladsl.server._ +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.eclair.{Kit, ShortChannelId} +import FormParamExtractors._ import akka.NotUsed -import akka.actor.{Actor, ActorRef, ActorSystem, Props, Scheduler} -import akka.http.scaladsl.model.HttpMethods._ -import akka.http.scaladsl.model._ +import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import akka.http.scaladsl.model.HttpMethods.POST +import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes} import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} -import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.http.scaladsl.server.directives.Credentials -import akka.http.scaladsl.server.directives.RouteDirectives.reject -import akka.pattern.ask -import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} +import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} import akka.stream.{ActorMaterializer, OverflowStrategy} -import akka.util.Timeout -import de.heikoseeberger.akkahttpjson4s.Json4sSupport -import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import fr.acinq.eclair.io.{NodeURI, Peer} -import fr.acinq.eclair.payment.PaymentLifecycle._ -import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} -import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} -import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw} +import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} +import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} import grizzled.slf4j.Logging -import org.json4s.JsonAST.{JBool, JInt, JString} -import org.json4s.{JValue, jackson} import scodec.bits.ByteVector - -import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -// @formatter:off -case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "eclair-node", method: String, params: Seq[JValue]) -case class Error(code: Int, message: String) -case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String) -case class Status(node_id: String) -case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) -case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) -trait RPCRejection extends Rejection { - def requestId: String -} -final case class UnknownMethodRejection(requestId: String) extends RPCRejection -final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection -final case class RpcValidationRejection(requestId: String, message: String) extends RPCRejection -final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection -// @formatter:on - -trait Service extends Logging with MetaService { - - implicit def ec: ExecutionContext = ExecutionContext.Implicits.global - - def scheduler: Scheduler +import scala.concurrent.duration._ - implicit val serialization = jackson.Serialization - implicit val formats = org.json4s.DefaultFormats + new ByteVectorSerializer + new ByteVector32Serializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer +new PaymentRequestSerializer - implicit val timeout = Timeout(60 seconds) - implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True +trait Service extends Directives with Logging with MetaService { - import Json4sSupport.{marshaller, unmarshaller} + // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 + import JsonSupport.marshaller + import JsonSupport.formats + import JsonSupport.serialization def password: String - def appKit: Kit + def eclairApi: EclairApi - val socketHandler: Flow[Message, TextMessage.Strict, NotUsed] + implicit val actorSystem: ActorSystem + implicit val mat: ActorMaterializer - def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { - case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) - case _ => akka.pattern.after(1 second, using = scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force + // a named and typed URL parameter used across several routes, 32-bytes hex-encoded + val channelIdNamedParameter = "channelId".as[ByteVector32](sha256HashUnmarshaller) + val shortChannelIdNamedParameter = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) + + val apiExceptionHandler = ExceptionHandler { + case t: Throwable => + logger.error(s"API call failed with cause=${t.getMessage}", t) + complete(StatusCodes.InternalServerError, s"Error: $t") } val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: `Access-Control-Allow-Methods`(POST) :: `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil - val myExceptionHandler = ExceptionHandler { - case t: Throwable => - extractRequest { _ => - logger.error(s"API call failed with cause=${t.getMessage}") - complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), "-1")) - } - } - - def completeRpcFuture(requestId: String, future: Future[AnyRef]): Route = onComplete(future) { - case Success(s) => completeRpc(requestId, s) - case Failure(t) => reject(ExceptionRejection(requestId, t.getLocalizedMessage)) - } - - def completeRpc(requestId: String, result: AnyRef): Route = complete(JsonRPCRes(result, None, requestId)) - - val myRejectionHandler: RejectionHandler = RejectionHandler.newBuilder() - .handleNotFound { - complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), "-1")) - } - .handle { - case _: AuthenticationFailedRejection ⇒ complete(StatusCodes.Unauthorized, JsonRPCRes(null, Some(Error(StatusCodes.Unauthorized.intValue, "Access restricted")), "-1")) - case v: RpcValidationRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId)) - case ukm: UnknownMethodRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, "method not found")), ukm.requestId)) - case p: UnknownParamsRejection ⇒ complete(StatusCodes.BadRequest, - JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"invalid parameters for this method, should be: ${p.message}")), p.requestId)) - case m: MalformedRequestContentRejection ⇒ complete(StatusCodes.BadRequest, - JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"malformed parameters for this method: ${m.message}")), "-1")) - case e: ExceptionRejection ⇒ complete(StatusCodes.BadRequest, - JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"command failed: ${e.message}")), e.requestId)) - case r ⇒ logger.error(s"API call failed with cause=$r") - complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1")) - } - .result() - - val route: Route = - respondWithDefaultHeaders(customHeaders) { - withRequestTimeoutResponse(r => HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """)) { - handleExceptions(myExceptionHandler) { - handleRejections(myRejectionHandler) { - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => - pathSingleSlash { - post { - entity(as[JsonRPCBody]) { - req => - val kit = appKit - import kit._ - - req.method match { - // utility methods - case "getinfo" => completeRpcFuture(req.id, getInfoResponse) - case "help" => completeRpc(req.id, help) + lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { - // channel lifecycle methods - case "connect" => req.params match { - case JString(pubkey) :: JString(host) :: JInt(port) :: Nil => - completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String]) - case JString(uri) :: Nil => - completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) - case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]")) - } - case "open" => req.params match { - case JString(nodeId) :: JInt(fundingSatoshis) :: Nil => - completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(0), fundingTxFeeratePerKw_opt = None, channelFlags = None)).mapTo[String]) - case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: Nil => - completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None, fundingTxFeeratePerKw_opt = None)).mapTo[String]) - case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: JInt(fundingFeerateSatPerByte) :: Nil => - completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = None)).mapTo[String]) - case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: JInt(fundingFeerateSatPerByte) :: JInt(flags) :: Nil => - completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = Some(flags.toByte))).mapTo[String]) - case _ => reject(UnknownParamsRejection(req.id, s"[nodeId, fundingSatoshis], [nodeId, fundingSatoshis, pushMsat], [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte] or [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte, flag]")) - } - case "close" => req.params match { - case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String]) - case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(ByteVector.fromValidHex(scriptPubKey)))).mapTo[String]) - case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]")) - } - case "forceclose" => req.params match { - case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_FORCECLOSE).mapTo[String]) - case _ => reject(UnknownParamsRejection(req.id, "[channelId]")) - } - case "updaterelayfee" => req.params match { - case JString(identifier) :: JInt(feeBaseMsat) :: JInt(feeProportionalMillionths) :: Nil => - completeRpcFuture(req.id, sendToChannel(identifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat.toLong, feeProportionalMillionths.toLong)).mapTo[String]) - case JString(identifier) :: JString(feeBaseMsat) :: JString(feeProportionalMillionths) :: Nil => - completeRpcFuture(req.id, sendToChannel(identifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat.toLong, feeProportionalMillionths.toLong)).mapTo[String]) - case _ => reject(UnknownParamsRejection(req.id, "[channelId] [feeBaseMsat] [feeProportionalMillionths]")) - } - // local network methods - case "peers" => completeRpcFuture(req.id, for { - peers <- (switchboard ? 'peers).mapTo[Iterable[ActorRef]] - peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) - } yield peerinfos) - case "channels" => req.params match { - case Nil => - val f = for { - channels_id <- (register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys) - channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) - } yield channels - completeRpcFuture(req.id, f) - case JString(remoteNodeId) :: Nil => Try(PublicKey(ByteVector.fromValidHex(remoteNodeId))) match { - case Success(pk) => - val f = for { - channels_id <- (register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys) - channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO])) - } yield channels - completeRpcFuture(req.id, f) - case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'")) - } - case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]")) - } - case "channel" => req.params match { - case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO]) - case _ => reject(UnknownParamsRejection(req.id, "[channelId]")) - } - - // global network methods - case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]]) - case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)))) - case "allupdates" => req.params match { - case JString(nodeId) :: Nil => Try(PublicKey(ByteVector.fromValidHex(nodeId))) match { - case Success(pk) => completeRpcFuture(req.id, (router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values)) - case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$nodeId'")) - } - case _ => completeRpcFuture(req.id, (router ? 'updates).mapTo[Iterable[ChannelUpdate]]) - } - - // payment methods - case "receive" => req.params match { - // only the payment description is given: user may want to generate a donation payment request - case JString(description) :: Nil => - completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write)) - // the amount is now given with the description - case JInt(amountMsat) :: JString(description) :: Nil => - completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write)) - case JInt(amountMsat) :: JString(description) :: JInt(expirySeconds) :: Nil => - completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description, Some(expirySeconds.toLong))).mapTo[PaymentRequest].map(PaymentRequest.write)) - case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description] or [amount, description, expiryDuration]")) - } - - // checkinvoice deprecated. - case "parseinvoice" | "checkinvoice" => req.params match { - case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match { - case Success(pr) => completeRpc(req.id,pr) - case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getMessage}")) - } - case _ => reject(UnknownParamsRejection(req.id, "[payment_request]")) - } - - case "findroute" => req.params match { - case JString(nodeId) :: JInt(amountMsat) :: Nil if nodeId.length() == 66 => Try(PublicKey(ByteVector.fromValidHex(nodeId))) match { - case Success(pk) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, pk, amountMsat.toLong)).mapTo[RouteResponse]) - case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid nodeId hash '$nodeId'")) - } - case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match { - case Success(PaymentRequest(_, Some(amountMsat), _, nodeId , _, _)) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat.toLong)).mapTo[RouteResponse]) - case Success(_) => reject(RpcValidationRejection(req.id, s"payment request is missing amount, please specify it")) - case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getLocalizedMessage}")) - } - case JString(paymentRequest) :: JInt(amountMsat) :: Nil => Try(PaymentRequest.read(paymentRequest)) match { - case Success(PaymentRequest(_, None, _, nodeId , _, _)) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat.toLong)).mapTo[RouteResponse]) - case Success(_) => reject(RpcValidationRejection(req.id, s"amount was specified both in payment request and api call")) - case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getLocalizedMessage}")) - } - case _ => reject(UnknownParamsRejection(req.id, "[payment_request] or [payment_request, amountMsat] or [nodeId, amountMsat]")) - } - - case "send" => req.params match { - // user manually sets the payment information - case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil => - (Try(ByteVector32.fromValidHex(paymentHash)), Try(PublicKey(ByteVector.fromValidHex(nodeId)))) match { - case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ? - SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult].map { - case s: PaymentSucceeded => s - case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) - }) - case (Failure(_), _) => reject(RpcValidationRejection(req.id, s"invalid payment hash '$paymentHash'")) - case _ => reject(RpcValidationRejection(req.id, s"invalid node id '$nodeId'")) - } - // user gives a Lightning payment request - case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match { - case Success(pr) => - // setting the payment amount - val amount_msat: Long = (pr.amount, rest) match { - // optional amount always overrides the amount in the payment request - case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong - case (Some(amount_msat_pr), _) => amount_msat_pr.amount - case _ => throw new RuntimeException("you must manually specify an amount for this payment request") - } - logger.debug(s"api call for sending payment with amount_msat=$amount_msat") - // optional cltv expiry - val sendPayment = pr.minFinalCltvExpiry match { - case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId) - case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry) - } - completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult].map { - case s: PaymentSucceeded => s - case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) - }) - case _ => reject(RpcValidationRejection(req.id, s"payment request is not valid")) - } - case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]")) - } + // create a flow transforming a queue of string -> string + val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() - // check received payments - case "checkpayment" => req.params match { - case JString(identifier) :: Nil => completeRpcFuture(req.id, for { - paymentHash <- Try(PaymentRequest.read(identifier)) match { - case Success(pr) => Future.successful(pr.paymentHash) - case _ => Try(ByteVector.fromValidHex(identifier)) match { - case Success(s) => Future.successful(s) - case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash")) - } - } - found <- (paymentHandler ? CheckPayment(ByteVector32.fromValidHex(identifier))).map(found => new JBool(found.asInstanceOf[Boolean])) - } yield found) - case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]")) - } + // register an actor that feeds the queue when a payment is received + actorSystem.actorOf(Props(new Actor { + override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) - // retrieve audit events - case "audit" => - val (from, to) = req.params match { - case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong) - case _ => (0L, Long.MaxValue) - } - completeRpcFuture(req.id, Future(AuditResponse( - sent = nodeParams.auditDb.listSent(from, to), - received = nodeParams.auditDb.listReceived(from, to), - relayed = nodeParams.auditDb.listRelayed(from, to)) - )) + def receive: Receive = { + case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) + } + })) - case "networkfees" => - val (from, to) = req.params match { - case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong) - case _ => (0L, Long.MaxValue) - } - completeRpcFuture(req.id, Future(nodeParams.auditDb.listNetworkFees(from, to))) + Flow[Message] + .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client + .merge(flowOutput) // Stream the data we want to the client + .map(TextMessage.apply) + } - // retrieve fee stats - case "channelstats" => completeRpcFuture(req.id, Future(nodeParams.auditDb.stats)) + val timeoutResponse: HttpRequest => HttpResponse = { r => + HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, "request timed out") + } + def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { + case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) + case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force + } - // method name was not found - case _ => reject(UnknownMethodRejection(req.id)) - } + val route: Route = { + respondWithDefaultHeaders(customHeaders) { + handleExceptions(apiExceptionHandler) { + withRequestTimeoutResponse(timeoutResponse) { + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => + post { + path("getinfo") { + complete(eclairApi.getInfoResponse()) + } ~ + path("help") { + complete(help) + } ~ + path("connect") { + formFields("uri".as[String]) { uri => + complete(eclairApi.connect(uri)) + } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => + complete(eclairApi.connect(s"$nodeId@$host:$port")) + } + } ~ + path("open") { + formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) + } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => + complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) + } + } ~ + path("forceclose") { + formFields(channelIdNamedParameter) { channelId => + complete(eclairApi.forceClose(Left(channelId))) + } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => + complete(eclairApi.forceClose(Right(shortChannelId))) + } + } ~ + path("updaterelayfee") { + formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(eclairApi.peersInfo()) + } ~ + path("channels") { + formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + formFields(channelIdNamedParameter) { channelId => + complete(eclairApi.channelInfo(channelId)) + } + } ~ + path("allnodes") { + complete(eclairApi.allnodes()) + } ~ + path("allchannels") { + complete(eclairApi.allchannels()) + } ~ + path("allupdates") { + formFields("nodeId".as[PublicKey].?) { nodeId_opt => + complete(eclairApi.allupdates(nodeId_opt)) + } + } ~ + path("receive") { + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(eclairApi.receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + formFields("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findRouteByInvoice") { + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) } + } ~ path("findRouteByNode") { + formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ + path("sendToInvoice") { + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => + complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + case (invoice, Some(overrideAmount)) => + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendToNode") { + formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + complete(eclairApi.send(nodeId, amountMsat, paymentHash)) + } + } ~ + path("checkpayment") { + formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => + complete(eclairApi.checkpayment(paymentHash)) + } ~ formFields("invoice".as[PaymentRequest]) { invoice => + complete(eclairApi.checkpayment(invoice.paymentHash)) + } + } ~ + path("audit") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(eclairApi.audit(from, to)) + } + } ~ + path("networkfees") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(eclairApi.networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(eclairApi.channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) } - } - } ~ path("ws") { - handleWebSocketMessages(socketHandler) } } } } } - - def getInfoResponse: Future[GetInfoResponse] - - def makeSocketHandler(system: ActorSystem)(implicit materializer: ActorMaterializer): Flow[Message, TextMessage.Strict, NotUsed] = { - - // create a flow transforming a queue of string -> string - val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() - - // register an actor that feeds the queue when a payment is received - system.actorOf(Props(new Actor { - override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) - def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) } - })) - - Flow[Message] - .mapConcat(_ => Nil) // Ignore heartbeats and other data from the client - .merge(flowOutput) // Stream the data we want to the client - .map(TextMessage.apply) } - def help = List( + val help = List( "connect (uri): open a secure connection to a lightning node", "connect (nodeId, host, port): open a secure connection to a lightning node", "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", @@ -405,18 +246,5 @@ trait Service extends Logging with MetaService { "getinfo: returns info about the blockchain and this node", "help: display this message") - /** - * Sends a request to a channel and expects a response - * - * @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) - * @param request - * @return - */ - def sendToChannel(channelIdentifier: String, request: Any): Future[Any] = - for { - fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request)) - .recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) } - .recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) } - res <- appKit.register ? fwdReq - } yield res -} + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index dd9e14e1fa..1cef7f6d93 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -17,9 +17,7 @@ package fr.acinq.eclair.api -import java.io.{File, FileOutputStream, PrintWriter} import java.nio.file.{Files, Path, Paths, StandardOpenOption} - import akka.actor.{Actor, ActorSystem, Props, Scheduler} import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ @@ -76,7 +74,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } } - class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends NewService { + class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends Service { override def eclairApi: EclairApi = new EclairApiImpl(kit, getInfoResp) override def password: String = "mock" From 709298998ba315871a951df8cbb4bb035bb6f33a Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 11:06:24 +0100 Subject: [PATCH 51/75] Remove MetaService in favor of a shorter syntax --- .../main/scala/fr/acinq/eclair/Setup.scala | 39 +++++++------------ .../scala/fr/acinq/eclair/api/EclairApi.scala | 11 +++++- .../fr/acinq/eclair/api/MetaService.scala | 10 ----- .../fr/acinq/eclair/api/OldService.scala | 2 +- .../scala/fr/acinq/eclair/api/Service.scala | 2 +- 5 files changed, 24 insertions(+), 40 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index e4a309dfd5..73387f09a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -270,40 +270,27 @@ class Setup(datadir: File, chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue(), publicAddresses = nodeParams.publicAddresses) - - val api = if (!config.getBoolean("api.use-old-api")) { + val apiPassword = config.getString("api.password") match { + case "" => throw EmptyAPIPasswordException + case valid => valid + } + val apiRoute = if (!config.getBoolean("api.use-old-api")) { new Service { - - override val actorSystem = kit.system - - override val mat = materializer - - override val password = { - val p = config.getString("api.password") - if (p.isEmpty) throw EmptyAPIPasswordException else p - } - - override def eclairApi: EclairApi = new EclairApiImpl(kit, getInfo) - - } + val actorSystem = kit.system + val mat = materializer + val password = apiPassword + def eclairApi: EclairApi = new EclairApiImpl(kit, getInfo) + }.route } else { new OldService { - override def scheduler = system.scheduler - - override val password = { - val p = config.getString("api.password") - if (p.isEmpty) throw EmptyAPIPasswordException else p - } - + override val password = apiPassword override def getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo) - override def appKit: Kit = kit - override val socketHandler = makeSocketHandler(system)(materializer) - } + }.route } - val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover { + val httpBound = Http().bindAndHandle(apiRoute, config.getString("api.binding-ip"), config.getInt("api.port")).recover { case _: BindFailedException => throw TCPBindException(config.getInt("api.port")) } val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port")))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala index 484a3fda9f..652d31edb9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -4,7 +4,7 @@ import akka.util.Timeout import akka.pattern._ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} -import fr.acinq.eclair.{Kit, ShortChannelId} +import fr.acinq.eclair.{Globals, Kit, ShortChannelId} import fr.acinq.eclair.io.{NodeURI, Peer} import akka.actor.{Actor, ActorRef, ActorSystem, Props} import fr.acinq.eclair.channel._ @@ -185,6 +185,13 @@ class EclairApiImpl (appKit: Kit, getInfo: GetInfoResponse) extends EclairApi { res <- appKit.register ? fwdReq } yield res - override def getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo) + override def getInfoResponse: Future[GetInfoResponse] = Future.successful( + GetInfoResponse(nodeId = appKit.nodeParams.nodeId, + alias = appKit.nodeParams.alias, + port = 8080, + chainHash = appKit.nodeParams.chainHash, + blockHeight = Globals.blockCount.intValue(), + publicAddresses = appKit.nodeParams.publicAddresses) + ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala deleted file mode 100644 index d3b820aefd..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/MetaService.scala +++ /dev/null @@ -1,10 +0,0 @@ -package fr.acinq.eclair.api - -import akka.http.scaladsl.server.Route - -// TODO remove this as soon as we remove Service.scala -trait MetaService { - - val route: Route - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala index f02da14df0..8e0a421f61 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -68,7 +68,7 @@ final case class RpcValidationRejection(requestId: String, message: String) exte final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection // @formatter:on -trait OldService extends Logging with MetaService { +trait OldService extends Logging { implicit def ec: ExecutionContext = ExecutionContext.Implicits.global diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 6793dafed4..dc631a5db0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -21,7 +21,7 @@ import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -trait Service extends Directives with Logging with MetaService { +trait Service extends Directives with Logging { // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 import JsonSupport.marshaller From 8567979a858d2415039494f801cc79e28a48e9ea Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 11:30:44 +0100 Subject: [PATCH 52/75] Remove getInfo parameter from EclairApi, remove 'port' from GetInfoResponse --- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 3 +-- eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala | 3 +-- .../src/main/scala/fr/acinq/eclair/api/OldService.scala | 2 +- eclair-core/src/test/resources/api/getinfo | 2 +- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 73387f09a0..54def5917c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -266,7 +266,6 @@ class Setup(datadir: File, implicit val materializer = ActorMaterializer() val getInfo = GetInfoResponse(nodeId = nodeParams.nodeId, alias = nodeParams.alias, - port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue(), publicAddresses = nodeParams.publicAddresses) @@ -279,7 +278,7 @@ class Setup(datadir: File, val actorSystem = kit.system val mat = materializer val password = apiPassword - def eclairApi: EclairApi = new EclairApiImpl(kit, getInfo) + def eclairApi: EclairApi = new EclairApiImpl(kit) }.route } else { new OldService { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala index 652d31edb9..b09b119930 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala @@ -60,7 +60,7 @@ trait EclairApi { } -class EclairApiImpl (appKit: Kit, getInfo: GetInfoResponse) extends EclairApi { +class EclairApiImpl (appKit: Kit) extends EclairApi { implicit val ec = appKit.system.dispatcher implicit val timeout = Timeout(60 seconds) // used by akka ask @@ -188,7 +188,6 @@ class EclairApiImpl (appKit: Kit, getInfo: GetInfoResponse) extends EclairApi { override def getInfoResponse: Future[GetInfoResponse] = Future.successful( GetInfoResponse(nodeId = appKit.nodeParams.nodeId, alias = appKit.nodeParams.alias, - port = 8080, chainHash = appKit.nodeParams.chainHash, blockHeight = Globals.blockCount.intValue(), publicAddresses = appKit.nodeParams.publicAddresses) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala index 8e0a421f61..4d6fd03836 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -57,7 +57,7 @@ case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "eclair-node", meth case class Error(code: Int, message: String) case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String) case class Status(node_id: String) -case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) +case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) trait RPCRejection extends Rejection { def requestId: String diff --git a/eclair-core/src/test/resources/api/getinfo b/eclair-core/src/test/resources/api/getinfo index f168177ed3..27952ae9d7 100644 --- a/eclair-core/src/test/resources/api/getinfo +++ b/eclair-core/src/test/resources/api/getinfo @@ -1 +1 @@ -{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","port":9735,"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":123456,"publicAddresses":["localhost:9731"]} \ No newline at end of file +{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":0,"publicAddresses":["localhost:9731"]} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 1cef7f6d93..bcb4a3aabd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -62,7 +62,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { def defaultGetInfo = GetInfoResponse( nodeId = Alice.nodeParams.nodeId, alias = Alice.nodeParams.alias, - port = 9735, chainHash = Alice.nodeParams.chainHash, blockHeight = 123456, publicAddresses = Alice.nodeParams.publicAddresses @@ -75,7 +74,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends Service { - override def eclairApi: EclairApi = new EclairApiImpl(kit, getInfoResp) + override def eclairApi: EclairApi = new EclairApiImpl(kit) override def password: String = "mock" From 3bf00fb5b221bf37dce6155b36cb22ad7e92fe13 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 14:39:05 +0100 Subject: [PATCH 53/75] Rename EclairApi into Eclair and move it to root package --- .../{api/EclairApi.scala => Eclair.scala} | 23 ++++++++++--------- .../main/scala/fr/acinq/eclair/Setup.scala | 14 +++++------ .../scala/fr/acinq/eclair/api/Service.scala | 5 ++-- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 6 +++-- 4 files changed, 26 insertions(+), 22 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/{api/EclairApi.scala => Eclair.scala} (95%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala similarity index 95% rename from eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index b09b119930..5ca90ea35f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/EclairApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -1,24 +1,25 @@ -package fr.acinq.eclair.api +package fr.acinq.eclair -import akka.util.Timeout +import akka.actor.ActorRef import akka.pattern._ +import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} -import fr.acinq.eclair.{Globals, Kit, ShortChannelId} -import fr.acinq.eclair.io.{NodeURI, Peer} -import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import fr.acinq.eclair.api.{AuditResponse, GetInfoResponse} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{NetworkFee, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} +import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ -import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} -import fr.acinq.eclair.router.{ChannelDesc, RouteNotFound, RouteRequest, RouteResponse} -import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} +import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} +import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} import scodec.bits.ByteVector -import scala.concurrent.duration._ + import scala.concurrent.Future +import scala.concurrent.duration._ -trait EclairApi { +trait Eclair { def connect(uri: String): Future[String] @@ -60,7 +61,7 @@ trait EclairApi { } -class EclairApiImpl (appKit: Kit) extends EclairApi { +class EclairApiImpl (appKit: Kit) extends Eclair { implicit val ec = appKit.system.dispatcher implicit val timeout = Timeout(60 seconds) // used by akka ask diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 54def5917c..890d549882 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -275,17 +275,17 @@ class Setup(datadir: File, } val apiRoute = if (!config.getBoolean("api.use-old-api")) { new Service { - val actorSystem = kit.system - val mat = materializer - val password = apiPassword - def eclairApi: EclairApi = new EclairApiImpl(kit) + override val actorSystem = kit.system + override val mat = materializer + override val password = apiPassword + override val eclairApi: Eclair = new EclairApiImpl(kit) }.route } else { new OldService { - override def scheduler = system.scheduler + override val scheduler = system.scheduler override val password = apiPassword - override def getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo) - override def appKit: Kit = kit + override val getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo) + override val appKit: Kit = kit override val socketHandler = makeSocketHandler(system)(materializer) }.route } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index dc631a5db0..9a2840da16 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.api import akka.http.scaladsl.server._ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} -import fr.acinq.eclair.{Kit, ShortChannelId} +import fr.acinq.eclair.{Eclair, Kit, ShortChannelId} import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} @@ -18,6 +18,7 @@ import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} import grizzled.slf4j.Logging import scodec.bits.ByteVector + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -30,7 +31,7 @@ trait Service extends Directives with Logging { def password: String - def eclairApi: EclairApi + val eclairApi: Eclair implicit val actorSystem: ActorSystem implicit val mat: ActorMaterializer diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index bcb4a3aabd..fc0c3b1abb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -18,12 +18,13 @@ package fr.acinq.eclair.api import java.nio.file.{Files, Path, Paths, StandardOpenOption} + import akka.actor.{Actor, ActorSystem, Props, Scheduler} import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import fr.acinq.eclair.blockchain.TestWallet -import fr.acinq.eclair.{Kit, TestConstants} +import fr.acinq.eclair.{Eclair, EclairApiImpl, Kit, TestConstants} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import TestConstants._ import akka.http.scaladsl.model.headers.BasicHttpCredentials @@ -33,6 +34,7 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import org.json4s.{Formats, JValue} import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.eclair.io.Peer + import scala.concurrent.duration._ import scala.io.Source @@ -74,7 +76,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends Service { - override def eclairApi: EclairApi = new EclairApiImpl(kit) + override def eclairApi: Eclair = new EclairApiImpl(kit) override def password: String = "mock" From 26cc39e46760bf3a710b5804abb94d30176ade90 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 14:46:59 +0100 Subject: [PATCH 54/75] Standardize method names (all lowercase) --- eclair-core/eclair-cli | 12 ++++++------ .../src/main/scala/fr/acinq/eclair/api/Service.scala | 8 ++++---- .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index f3d16f918b..a7eef2700e 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -106,15 +106,15 @@ case ${METHOD}_${#} in "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; - "sendToInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "sendToInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "sendToNode_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; + "send_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "send_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "sendtonode_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; "parseinvoice_1") call ${METHOD} "$(printf invoice=%s ${1})" ;; - "findRouteByInvoice_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "findRouteByInvoice_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "findRouteByNode_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "findroute_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; + "findroute_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; + "findroutetonode_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; "checkpayment_1") call "checkpayment" "$(printf invoice=%s ${1})" ;; "checkpaymentbyhash_1") call "checkpayment" "$(printf paymentHash=%s ${1})" ;; # calls checkinvoice but using the paymentHash instead of the invoice diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 9a2840da16..1663d85cc9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -157,18 +157,18 @@ trait Service extends Directives with Logging { complete(invoice) } } ~ - path("findRouteByInvoice") { + path("findroute") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) } - } ~ path("findRouteByNode") { + } ~ path("findroutetonode") { formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => complete(eclairApi.findRoute(nodeId, amount)) } } ~ - path("sendToInvoice") { + path("send") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) @@ -177,7 +177,7 @@ trait Service extends Directives with Logging { case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) } } ~ - path("sendToNode") { + path("sendtonode") { formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index fc0c3b1abb..0a69ab1d40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -76,10 +76,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends Service { - override def eclairApi: Eclair = new EclairApiImpl(kit) - + override val eclairApi: Eclair = new EclairApiImpl(kit) override def password: String = "mock" - override implicit val actorSystem: ActorSystem = system override implicit val mat: ActorMaterializer = materializer } From 27c2c83c2c8592ab6febbac80cd45e32daf4b48a Mon Sep 17 00:00:00 2001 From: dpad85 Date: Fri, 22 Mar 2019 15:34:59 +0100 Subject: [PATCH 55/75] Updated eclair-cli to work with the revamped API File is a bit longer, but with simpler logic since parameters are now named, and order does not matter anymore. Command and its parameters are transformed into endpoint + url encoded body (param1=xxx¶m2=yyy...). Removed verbose option and added a -c option which prints colored JSON (off by default). Help message does not rely on API /help endpoint anymore and contains a list of all the available commands. --- eclair-core/eclair-cli | 218 ++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 120 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index a7eef2700e..13abf0c8bd 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -1,132 +1,110 @@ #!/bin/bash + +# default script values, can be overriden for convenience. +api_url='http://localhost:8081' +# api_password='your_api_password' # uncomment this if you don't want to provide a password each time you call eclair-cli +colors=false + +# prints help message +usage() { + echo -e "============================== +Command line client for eclair +============================== +This tool requires the eclair node's API to be enabled and listening +on <$api_url>. + +Usage +----- +\e[93meclair-cli\e[39m [\e[93mOPTIONS\e[39m]... [\e[93mCOMMAND\e[39m] [--command-param command-value]... + +where OPTIONS can be: + -p API's password + -a
Override the API URL with
+ -c Outputs colored JSON + +and COMMAND is one of: + getinfo, connect, open, close, forceclose, updaterelayfee, + peers, channels, channel, allnodes, allchannels, allupdates, + receive, parseinvoice, findroute, findroutetonode, + send, sendtonode, checkpayment, + audit, networkfees, channelstats + +Examples +-------- + eclair-cli getinfo get node info from $api_url + eclair-cli -a localhost:1234 peers list the peers of a node hosted on localhost:1234 + eclair-cli close --channelId 006fb... closes the channel with id 006fb... + + +Full documentation at: " 1>&2; exit 1; +} + +# -- script's logic begins here + # Check if jq is installed. If not, display instructions and abort program command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installation instructions, visit https://stedolan.github.io/jq/download/.\n\nAborting..."; exit 1; } # curl installed? If not, give a hint command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborting..."; exit 1; } -FULL_OUTPUT='false' -URL='http://localhost:8080' -PASSWORD='' - -# -------------------- METHODS - -displayhelp() { - echo -e "Usage: eclair-cli [OPTION]... [COMMAND] -Client for an eclair node. - -With COMMAND is one of the command listed by \e[01;33meclair-cli help\e[0m. - - -p api's password - -a
Override the api URL with
- -v Outputs full json returned by the API - -Examples: - eclair-cli help display available commands - eclair-cli -a localhost:1234 peers list the peers - eclair-cli close 006fb... closes the channel with id 006fb... - -Note: Uses the json-rpc api exposed by the node on localhost:8080. Make sure the api is enabled. -Full documentation at: " -} - -# Executes a JSON RPC call to a node listening on ${URL} -call() { - jqexp='.' - # override default jq parsing expression - if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi - - # set password - if [ -z ${PASSWORD} ]; then auth="eclair-cli"; - else auth="eclair-cli:"${PASSWORD}; fi - # collect form data from 2nd parameter - form_data="" - for param in ${2}; do - form_data="$form_data -F \"$param\"" - done; - - eval curl "--user ${auth} --silent --show-error -X POST $form_data ${URL}/${1}" | jq -r "$jqexp" -} - -# get script options -while getopts 'vu:p:a:' flag; do - case "${flag}" in - p) PASSWORD="${OPTARG}" ;; - a) URL="${OPTARG}" ;; - v) FULL_OUTPUT="true" ;; - *) echo -e "\nAborting..."; exit 1; ;; - esac +# extract script options +while getopts ':cu:p:a:' flag; do + case "${flag}" in + p) api_password="${OPTARG}" ;; + a) api_url="${OPTARG}" ;; + c) colors=true ;; + *) ;; + esac done - shift $(($OPTIND - 1)) -# assigning JSON RPC method and params values from arguments -METHOD=${1} +# extract api's endpoint (e.g. sendpayment, connect, ...) from params +api_endpoint=${1} shift 1 - -# Whatever the arguments provided to eclair-cli, a call to the API will be sent. Let it fail! -case ${METHOD}_${#} in - ""_*) displayhelp ;; - "help"*) displayhelp - echo -e "\nAvailable commands:\n" - call "help" "" ;; - - "getinfo_0") call ${METHOD} "" ;; - - "connect_1") call ${METHOD} "$(printf uri=%s ${1})" ;; - "connect_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf host=%s ${2})" "$(printf port=%s ${3})" " ;; - - "open_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" " ;; - "open_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" " ;; - "open_4") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" " ;; - "open_5") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf fundingSatoshis=%s ${2})" "$(printf pushMsat=%s ${3})" "$(printf channelFlags=%s ${4})" "$(printf fundingFeerateSatByte=%s ${5})" " ;; - - "close_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; - "close_2") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf scriptPubKey=%s ${2})" " ;; - - "forceclose_1") call ${METHOD} "$(printf channelId=%s ${1})" ;; - - "updaterelayfee_3") call ${METHOD} " "$(printf channelId=%s ${1})" "$(printf feeBaseMsat=%s ${2})" "$(printf feeProportionalMillionths=%s ${3})" " ;; - - "peers_0") call ${METHOD} "" ;; - - "channel_1") call ${METHOD} "$(printf channelId=%s ${1})" ". | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }" ;; - - "channels_0") call ${METHOD} "" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - "channels_1") call ${METHOD} "$(printf toRemoteNodeId=%s ${1})" ". | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint } )" ;; - - "allnodes_0") call ${METHOD} "" ;; - - "allchannels_0") call ${METHOD} "" ;; - - "allupdates_0") call ${METHOD} "" ;; - "allupdates_1") call ${METHOD} "$(printf nodeId=%s ${1})" ;; - - "receive_2") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" " ;; - "receive_3") call ${METHOD} " "$(printf amountMsat=%s ${1})" "$(printf description=%s ${2})" "$(printf expireIn=%s ${3})" " ;; - - "send_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "send_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "sendtonode_3") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" "$(printf paymentHash=%s ${3})" " ;; - - "parseinvoice_1") call ${METHOD} "$(printf invoice=%s ${1})" ;; - - "findroute_1") call ${METHOD} " "$(printf invoice=%s ${1})" " ;; - "findroute_2") call ${METHOD} " "$(printf invoice=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - "findroutetonode_2") call ${METHOD} " "$(printf nodeId=%s ${1})" "$(printf amountMsat=%s ${2})" " ;; - - "checkpayment_1") call "checkpayment" "$(printf invoice=%s ${1})" ;; - "checkpaymentbyhash_1") call "checkpayment" "$(printf paymentHash=%s ${1})" ;; # calls checkinvoice but using the paymentHash instead of the invoice - - "audit_0") call ${METHOD} "" ;; - "audit_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; - - "networkfees_0") call ${METHOD} "" ;; - "networkfees_2") call ${METHOD} " "$(printf from=%s ${1})" "$(printf to=%s ${2})" " ;; - - "channelstats_0") call ${METHOD} "" ;; - - *) displayhelp ; exit 1 ;; # Default case. - -esac +# display a usage method if no method given, or help is requested +if [ -z $api_endpoint ] || [ $api_endpoint == "help" ] || [ $api_endpoint == "--help" ]; + then usage; +fi + +# transform long options into a HTTP encoded url body. +api_payload="" +index=1 +for arg in "${@}"; do + transformed_arg="" + case ${arg} in + "--"*) + # if arg begins with two dashes, it is the name of a parameter. Dashes must be removed, and arg must be followed by an equal sign + # also, it must be prefixed by an '&' sign, if it is not the first argument + if [ $index -eq 1 ]; + then transformed_arg="$transformed_arg${arg:2}=" + else transformed_arg="&$transformed_arg${arg:2}=" + fi + ;; + *) + transformed_arg=$arg + ;; + esac + api_payload="$api_payload$transformed_arg"; + let "index++" +done; + +# jq parses response body for error message +jq_filter='if .error == null then . else .error end' + +# jq options +if [ "$colors" = true ]; then + jq_opts="--color-output"; +else + jq_opts="--monochrome-output" +fi + +# if no password is provided, auth should only contain user login so that curl prompts for the api password +if [ -z $api_password ]; then + auth="eclair-cli"; +else + auth="eclair-cli:$api_password"; +fi + +# we're now ready to execute the API call +eval curl "--user $auth --silent --show-error -X POST -H \"Content-Type: application/x-www-form-urlencoded\" -d '$api_payload' $api_url/$api_endpoint" | jq -r "$jq_opts" "$jq_filter" From 1e15c92e7b03995f871f1a3f852006c19de6658e Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 15:55:27 +0100 Subject: [PATCH 56/75] Set blockHeight during test for getinfo --- eclair-core/src/test/resources/api/getinfo | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/test/resources/api/getinfo b/eclair-core/src/test/resources/api/getinfo index 27952ae9d7..1fbb200c3d 100644 --- a/eclair-core/src/test/resources/api/getinfo +++ b/eclair-core/src/test/resources/api/getinfo @@ -1 +1 @@ -{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":0,"publicAddresses":["localhost:9731"]} \ No newline at end of file +{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":9999,"publicAddresses":["localhost:9731"]} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 0a69ab1d40..70d81dcdff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -24,7 +24,7 @@ import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import fr.acinq.eclair.blockchain.TestWallet -import fr.acinq.eclair.{Eclair, EclairApiImpl, Kit, TestConstants} +import fr.acinq.eclair._ import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import TestConstants._ import akka.http.scaladsl.model.headers.BasicHttpCredentials @@ -61,21 +61,13 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { wallet = new TestWallet ) - def defaultGetInfo = GetInfoResponse( - nodeId = Alice.nodeParams.nodeId, - alias = Alice.nodeParams.alias, - chainHash = Alice.nodeParams.chainHash, - blockHeight = 123456, - publicAddresses = Alice.nodeParams.publicAddresses - ) - class MockActor extends Actor { override def receive: Receive = { case _ => } } - class MockService(kit: Kit = defaultMockKit, getInfoResp: GetInfoResponse = defaultGetInfo) extends Service { + class MockService(kit: Kit = defaultMockKit) extends Service { override val eclairApi: Eclair = new EclairApiImpl(kit) override def password: String = "mock" override implicit val actorSystem: ActorSystem = system @@ -133,6 +125,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } test("'help' should respond with a help message") { + Globals.blockCount.set(9999) val mockService = new MockService() Post("/help") ~> @@ -267,7 +260,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } } - private def matchTestJson(apiName: String, overWrite: Boolean, response: String)(implicit formats: Formats) = { + private def matchTestJson(apiName: String, overWrite: Boolean, response: String) = { val p = Paths.get(s"src/test/resources/api/$apiName") if (overWrite) { From 7f631045e98c95cb46033dbd4a6c875bc7de5a8d Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 16:34:17 +0100 Subject: [PATCH 57/75] Include help response in eclair-cli --- eclair-core/eclair-cli | 55 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 13abf0c8bd..319b90bdf8 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -23,7 +23,7 @@ where OPTIONS can be: -c Outputs colored JSON and COMMAND is one of: - getinfo, connect, open, close, forceclose, updaterelayfee, + help, getinfo, connect, open, close, forceclose, updaterelayfee, peers, channels, channel, allnodes, allchannels, allupdates, receive, parseinvoice, findroute, findroutetonode, send, sendtonode, checkpayment, @@ -31,7 +31,7 @@ and COMMAND is one of: Examples -------- - eclair-cli getinfo get node info from $api_url + eclair-cli help display available commands eclair-cli -a localhost:1234 peers list the peers of a node hosted on localhost:1234 eclair-cli close --channelId 006fb... closes the channel with id 006fb... @@ -39,6 +39,44 @@ Examples Full documentation at: " 1>&2; exit 1; } +help() { + echo -e " + connect (uri): open a secure connection to a lightning node + connect (nodeId, host, port): open a secure connection to a lightning node + open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced + updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel + peers: list existing local peers + channels: list existing local channels + channels (nodeId): list existing local channels to a particular nodeId + channel (channelId): retrieve detailed information about a given channel + channelstats: retrieves statistics about channel usage (fees, number and average amount of payments) + allnodes: list all known nodes + allchannels: list all known channels + allupdates: list all channels updates + allupdates (nodeId): list all channels updates for this nodeId + receive (amountMsat, description): generate a payment request for a given amount + receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires + parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request + findroute (paymentRequest): returns nodes and channels of the route if there is any + findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any + findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any + 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 + send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount + close (channelId): close a channel + close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey + forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable) + checkpayment (paymentHash): returns true if the payment has been received, false otherwise + checkpayment (paymentRequest): returns true if the payment has been received, false otherwise + audit: list all send/received/relayed payments + audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to) + networkfees: list all network fees paid to the miners, by transaction + networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to) + getinfo: returns info about the blockchain and this node + help: display this message + "; +} + # -- script's logic begins here # Check if jq is installed. If not, display instructions and abort program @@ -62,9 +100,16 @@ shift $(($OPTIND - 1)) api_endpoint=${1} shift 1 -# display a usage method if no method given, or help is requested -if [ -z $api_endpoint ] || [ $api_endpoint == "help" ] || [ $api_endpoint == "--help" ]; - then usage; +# display a usage method if no method given +if [ -z $api_endpoint ]; then + usage; + exit 0; +fi + +# display a help and exit +if [ "$api_endpoint" == "help" ]; then + help; + exit 0; fi # transform long options into a HTTP encoded url body. From 4787f0a497f03d6b6392dc073c3cbe14adb410c2 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 16:40:07 +0100 Subject: [PATCH 58/75] Remove 'help' method --- .../scala/fr/acinq/eclair/api/Service.scala | 38 ------------------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 22 ++--------- 2 files changed, 3 insertions(+), 57 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 1663d85cc9..2a30d10839 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -88,9 +88,6 @@ trait Service extends Directives with Logging { path("getinfo") { complete(eclairApi.getInfoResponse()) } ~ - path("help") { - complete(help) - } ~ path("connect") { formFields("uri".as[String]) { uri => complete(eclairApi.connect(uri)) @@ -212,40 +209,5 @@ trait Service extends Directives with Logging { } } - val help = List( - "connect (uri): open a secure connection to a lightning node", - "connect (nodeId, host, port): open a secure connection to a lightning node", - "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", - "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", - "peers: list existing local peers", - "channels: list existing local channels", - "channels (nodeId): list existing local channels to a particular nodeId", - "channel (channelId): retrieve detailed information about a given channel", - "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", - "allnodes: list all known nodes", - "allchannels: list all known channels", - "allupdates: list all channels updates", - "allupdates (nodeId): list all channels updates for this nodeId", - "receive (amountMsat, description): generate a payment request for a given amount", - "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", - "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", - "findroute (paymentRequest): returns nodes and channels of the route if there is any", - "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", - "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", - "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", - "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", - "close (channelId): close a channel", - "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", - "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", - "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", - "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", - "audit: list all send/received/relayed payments", - "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", - "networkfees: list all network fees paid to the miners, by transaction", - "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", - "getinfo: returns info about the blockchain and this node", - "help: display this message") - } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 70d81dcdff..9302c65193 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -78,7 +78,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { val mockService = new MockService // no auth - Post("/help") ~> + Post("/getinfo") ~> Route.seal(mockService.route) ~> check { assert(handled) @@ -86,7 +86,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } // wrong auth - Post("/help") ~> + Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password + "what!")) ~> Route.seal(mockService.route) ~> check { @@ -124,22 +124,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } - test("'help' should respond with a help message") { - Globals.blockCount.set(9999) - val mockService = new MockService() - - Post("/help") ~> - addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> - check { - assert(handled) - assert(status == OK) - val resp = entityAs[String] - matchTestJson("help", false, resp) - } - - } - test("'peers' should ask the switchboard for current known peers") { val mockAlicePeer = system.actorOf(Props(new {} with MockActor { @@ -183,11 +167,11 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } test("'getinfo' response should include this node ID") { + Globals.blockCount.set(9999) val mockService = new MockService() Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - addHeader("Content-Type", "application/json") ~> Route.seal(mockService.route) ~> check { assert(handled) From d6f243434999eff410847bd9aeda169abbd08109 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 16:48:45 +0100 Subject: [PATCH 59/75] Support -h in eclair-cli --- eclair-core/eclair-cli | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 319b90bdf8..18fede457c 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -21,6 +21,7 @@ where OPTIONS can be: -p API's password -a
Override the API URL with
-c Outputs colored JSON + -h Show available commands and COMMAND is one of: help, getinfo, connect, open, close, forceclose, updaterelayfee, @@ -75,6 +76,7 @@ help() { getinfo: returns info about the blockchain and this node help: display this message "; + exit 0; } # -- script's logic begins here @@ -86,11 +88,12 @@ command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installa command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborting..."; exit 1; } # extract script options -while getopts ':cu:p:a:' flag; do +while getopts ':cu:p:a:hu:' flag; do case "${flag}" in p) api_password="${OPTARG}" ;; a) api_url="${OPTARG}" ;; c) colors=true ;; + h) help ;; *) ;; esac done @@ -103,13 +106,11 @@ shift 1 # display a usage method if no method given if [ -z $api_endpoint ]; then usage; - exit 0; fi # display a help and exit if [ "$api_endpoint" == "help" ]; then help; - exit 0; fi # transform long options into a HTTP encoded url body. From 6d5a25ca29083a83ca3f9006eb9eac0791566948 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 17:19:53 +0100 Subject: [PATCH 60/75] Encapsule errors in a JSON response --- .../scala/fr/acinq/eclair/api/Service.scala | 257 +++++++++--------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 5 +- 2 files changed, 137 insertions(+), 125 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 2a30d10839..10e2520ddf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -8,7 +8,7 @@ import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.http.scaladsl.model.HttpMethods.POST -import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes} +import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} import akka.http.scaladsl.model.ws.{Message, TextMessage} @@ -18,10 +18,11 @@ import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} import grizzled.slf4j.Logging import scodec.bits.ByteVector - import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ +case class ErrorResponse(error: String) + trait Service extends Directives with Logging { // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 @@ -43,7 +44,13 @@ trait Service extends Directives with Logging { val apiExceptionHandler = ExceptionHandler { case t: Throwable => logger.error(s"API call failed with cause=${t.getMessage}", t) - complete(StatusCodes.InternalServerError, s"Error: $t") + complete(StatusCodes.InternalServerError, ErrorResponse(t.getMessage)) + } + + // map all the rejections to a JSON error object ErrorResponse + val apiRejectionHandler = RejectionHandler.default.mapRejectionResponse { + case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) => + res.copy(entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String)))) } val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: @@ -71,7 +78,7 @@ trait Service extends Directives with Logging { } val timeoutResponse: HttpRequest => HttpResponse = { r => - HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, "request timed out") + HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out"))) } def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { @@ -82,126 +89,128 @@ trait Service extends Directives with Logging { val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { - withRequestTimeoutResponse(timeoutResponse) { - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => - post { - path("getinfo") { - complete(eclairApi.getInfoResponse()) - } ~ - path("connect") { - formFields("uri".as[String]) { uri => - complete(eclairApi.connect(uri)) - } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => - complete(eclairApi.connect(s"$nodeId@$host:$port")) - } - } ~ - path("open") { - formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => - complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) - } - } ~ - path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => - complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) - } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => - complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) - } - } ~ - path("forceclose") { - formFields(channelIdNamedParameter) { channelId => - complete(eclairApi.forceClose(Left(channelId))) - } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => - complete(eclairApi.forceClose(Right(shortChannelId))) - } - } ~ - path("updaterelayfee") { - formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ - path("peers") { - complete(eclairApi.peersInfo()) - } ~ - path("channels") { - formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => - complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - formFields(channelIdNamedParameter) { channelId => - complete(eclairApi.channelInfo(channelId)) - } - } ~ - path("allnodes") { - complete(eclairApi.allnodes()) - } ~ - path("allchannels") { - complete(eclairApi.allchannels()) - } ~ - path("allupdates") { - formFields("nodeId".as[PublicKey].?) { nodeId_opt => - complete(eclairApi.allupdates(nodeId_opt)) - } - } ~ - path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(eclairApi.receive(desc, amountMsat, expire)) - } - } ~ - path("parseinvoice") { - formFields("invoice".as[PaymentRequest]) { invoice => - complete(invoice) - } - } ~ - path("findroute") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) - case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) - } - } ~ path("findroutetonode") { - formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) - } - } ~ - path("send") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => - complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case (invoice, Some(overrideAmount)) => - complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) - } - } ~ - path("sendtonode") { - formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => - complete(eclairApi.send(nodeId, amountMsat, paymentHash)) - } - } ~ - path("checkpayment") { - formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => - complete(eclairApi.checkpayment(paymentHash)) - } ~ formFields("invoice".as[PaymentRequest]) { invoice => - complete(eclairApi.checkpayment(invoice.paymentHash)) - } - } ~ - path("audit") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.audit(from, to)) - } - } ~ - path("networkfees") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.networkFees(from, to)) - } - } ~ - path("channelstats") { - complete(eclairApi.channelStats()) - } ~ - path("ws") { - handleWebSocketMessages(makeSocketHandler) - } + handleRejections(apiRejectionHandler){ + withRequestTimeoutResponse(timeoutResponse) { + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => + post { + path("getinfo") { + complete(eclairApi.getInfoResponse()) + } ~ + path("connect") { + formFields("uri".as[String]) { uri => + complete(eclairApi.connect(uri)) + } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => + complete(eclairApi.connect(s"$nodeId@$host:$port")) + } + } ~ + path("open") { + formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) + } + } ~ + path("close") { + formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) + } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => + complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) + } + } ~ + path("forceclose") { + formFields(channelIdNamedParameter) { channelId => + complete(eclairApi.forceClose(Left(channelId))) + } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => + complete(eclairApi.forceClose(Right(shortChannelId))) + } + } ~ + path("updaterelayfee") { + formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ + path("peers") { + complete(eclairApi.peersInfo()) + } ~ + path("channels") { + formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt => + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + formFields(channelIdNamedParameter) { channelId => + complete(eclairApi.channelInfo(channelId)) + } + } ~ + path("allnodes") { + complete(eclairApi.allnodes()) + } ~ + path("allchannels") { + complete(eclairApi.allchannels()) + } ~ + path("allupdates") { + formFields("nodeId".as[PublicKey].?) { nodeId_opt => + complete(eclairApi.allupdates(nodeId_opt)) + } + } ~ + path("receive") { + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => + complete(eclairApi.receive(desc, amountMsat, expire)) + } + } ~ + path("parseinvoice") { + formFields("invoice".as[PaymentRequest]) { invoice => + complete(invoice) + } + } ~ + path("findroute") { + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) + case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) + } + } ~ path("findroutetonode") { + formFields("nodeId".as[PublicKey], "amountMsat".as[Long]) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ + path("send") { + formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => + complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + case (invoice, Some(overrideAmount)) => + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendtonode") { + formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + complete(eclairApi.send(nodeId, amountMsat, paymentHash)) + } + } ~ + path("checkpayment") { + formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => + complete(eclairApi.checkpayment(paymentHash)) + } ~ formFields("invoice".as[PaymentRequest]) { invoice => + complete(eclairApi.checkpayment(invoice.paymentHash)) + } + } ~ + path("audit") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(eclairApi.audit(from, to)) + } + } ~ + path("networkfees") { + formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => + complete(eclairApi.networkFees(from, to)) + } + } ~ + path("channelstats") { + complete(eclairApi.channelStats()) + } ~ + path("ws") { + handleWebSocketMessages(makeSocketHandler) + } + } } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 9302c65193..0f999fcf47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -37,6 +37,7 @@ import fr.acinq.eclair.io.Peer import scala.concurrent.duration._ import scala.io.Source +import scala.reflect.ClassTag class ApiServiceSpec extends FunSuite with ScalatestRouteTest { @@ -110,7 +111,9 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { check { assert(handled) assert(status == BadRequest) - assert(entityAs[String].contains("The form field 'channelId' was malformed")) + val resp = entityAs[ErrorResponse](JsonSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) + println(resp.error) + assert(resp.error == "The form field 'channelId' was malformed:\nInvalid hexadecimal character 'h' at index 0") } // wrong params From e07e9f6d46804162b1a806c8b36af387fe86f3db Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 17:35:34 +0100 Subject: [PATCH 61/75] Rename named parameters for better readability --- .../main/scala/fr/acinq/eclair/api/Service.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 10e2520ddf..3003b1fa21 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -38,8 +38,8 @@ trait Service extends Directives with Logging { implicit val mat: ActorMaterializer // a named and typed URL parameter used across several routes, 32-bytes hex-encoded - val channelIdNamedParameter = "channelId".as[ByteVector32](sha256HashUnmarshaller) - val shortChannelIdNamedParameter = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) + val channelId = "channelId".as[ByteVector32](sha256HashUnmarshaller) + val shortChannelId = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) val apiExceptionHandler = ExceptionHandler { case t: Throwable => @@ -110,21 +110,21 @@ trait Service extends Directives with Logging { } } ~ path("close") { - formFields(channelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => + formFields(channelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) - } ~ formFields(shortChannelIdNamedParameter, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => + } ~ formFields(shortChannelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) => complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt)) } } ~ path("forceclose") { - formFields(channelIdNamedParameter) { channelId => + formFields(channelId) { channelId => complete(eclairApi.forceClose(Left(channelId))) - } ~ formFields(shortChannelIdNamedParameter) { shortChannelId => + } ~ formFields(shortChannelId) { shortChannelId => complete(eclairApi.forceClose(Right(shortChannelId))) } } ~ path("updaterelayfee") { - formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + formFields(channelId, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) } } ~ @@ -137,7 +137,7 @@ trait Service extends Directives with Logging { } } ~ path("channel") { - formFields(channelIdNamedParameter) { channelId => + formFields(channelId) { channelId => complete(eclairApi.channelInfo(channelId)) } } ~ From 9ca19bccdea96d477095cdb85367300d3a5d9c4c Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 22 Mar 2019 18:08:40 +0100 Subject: [PATCH 62/75] Rename EclairApiImpl to EclairImpl --- eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala | 2 +- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 5ca90ea35f..a7f32f2c09 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -61,7 +61,7 @@ trait Eclair { } -class EclairApiImpl (appKit: Kit) extends Eclair { +class EclairImpl(appKit: Kit) extends Eclair { implicit val ec = appKit.system.dispatcher implicit val timeout = Timeout(60 seconds) // used by akka ask diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 921208cc5c..15f15e4726 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -276,7 +276,7 @@ class Setup(datadir: File, override val actorSystem = kit.system override val mat = materializer override val password = apiPassword - override val eclairApi: Eclair = new EclairApiImpl(kit) + override val eclairApi: Eclair = new EclairImpl(kit) }.route } else { new OldService { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 0f999fcf47..5164bc8584 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -69,7 +69,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } class MockService(kit: Kit = defaultMockKit) extends Service { - override val eclairApi: Eclair = new EclairApiImpl(kit) + override val eclairApi: Eclair = new EclairImpl(kit) override def password: String = "mock" override implicit val actorSystem: ActorSystem = system override implicit val mat: ActorMaterializer = materializer From 00b357ab580c67f9a48ae5d46e7b3e2fc18cde48 Mon Sep 17 00:00:00 2001 From: dpad85 Date: Fri, 22 Mar 2019 18:38:58 +0100 Subject: [PATCH 63/75] Wip improving the help message in eclair-cli --- eclair-core/eclair-cli | 113 ++++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 18fede457c..1cf48fdcda 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -40,42 +40,85 @@ Examples Full documentation at: " 1>&2; exit 1; } +# prints a pretty command doc. 1st arg is command, 2nd arg is params, 3rd arg is the description +prettyPrintCommand() { + method=${1} + params="" + desc="" + shift 1 + for arg in "${@}"; do + case $arg in + ";"*) desc="${arg:1}";; + *) params="$params--$arg ";; + esac + done; + if [[ ${#params} -gt 40 ]]; then + printf " %-50s \n%-54s %s\n" "$method $params" "" "$desc" + else + printf " %-50s %s\n" "$method $params" "$desc" + fi +} + +# prints the list of available commands with parameters help() { - echo -e " - connect (uri): open a secure connection to a lightning node - connect (nodeId, host, port): open a secure connection to a lightning node - open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced - updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel - peers: list existing local peers - channels: list existing local channels - channels (nodeId): list existing local channels to a particular nodeId - channel (channelId): retrieve detailed information about a given channel - channelstats: retrieves statistics about channel usage (fees, number and average amount of payments) - allnodes: list all known nodes - allchannels: list all known channels - allupdates: list all channels updates - allupdates (nodeId): list all channels updates for this nodeId - receive (amountMsat, description): generate a payment request for a given amount - receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires - parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request - findroute (paymentRequest): returns nodes and channels of the route if there is any - findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any - findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any - 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 - send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount - close (channelId): close a channel - close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey - forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable) - checkpayment (paymentHash): returns true if the payment has been received, false otherwise - checkpayment (paymentRequest): returns true if the payment has been received, false otherwise - audit: list all send/received/relayed payments - audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to) - networkfees: list all network fees paid to the miners, by transaction - networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to) - getinfo: returns info about the blockchain and this node - help: display this message - "; + echo -e "\nAvailable commands:\n" + prettyPrintCommand "getinfo" ";Get node information" + echo "" + prettyPrintCommand "connect" "uri" ";Open a secure connection to a lightning node." + prettyPrintCommand "connect" "nodeId" "host" "port" + prettyPrintCommand "open" "nodeId" "fundingSatoshis" "pushMsat" "feerateSatPerByte" "channelFlags" ";Open a channel with another lightning node. Push, feerateSatPerByte and channelFlags are optional." + + echo "" + prettyPrintCommand "close" "channelId" ";Close channel." + prettyPrintCommand "close" "scriptPubKey" + prettyPrintCommand "forceclose" "channelId" ";Force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)." + + echo "" + prettyPrintCommand "peers" ";List nodes you have a channel with" + prettyPrintCommand "channels" ";List your local channels" + prettyPrintCommand "channels" "nodeId" ";List your local channels with a specific node" + prettyPrintCommand "channel" "channelId" ";Retrieve local channel detailed information." + + echo "" + prettyPrintCommand "updaterelayfee" "channelId" "feeBaseMsat" "feeProportionalMillionths" ";Update relay fee for payments going through this channel." + + echo "" + prettyPrintCommand "channelstats" ";Retrieve statistics about channel usage (fees, number and average amount of payments)." + + echo "" + prettyPrintCommand "allnodes" ";List all known nodes, channels, or updates in the network." + prettyPrintCommand "allchannels" + prettyPrintCommand "allupdates" + prettyPrintCommand "allupdates" "nodeId" + + echo "" + prettyPrintCommand "receive" "amountMsat" "description" ";Generate a Lightning payment request to receive funds." + prettyPrintCommand "receive" "amountMsat" "description" "expirySeconds" + + echo "" + prettyPrintCommand "send" "amountMsat" "paymentHash" "nodeId" ";Send a payment to a node, or with a Lightning payment request." + prettyPrintCommand "send" "paymentRequest" + prettyPrintCommand "send" "paymentRequest" "amountMsat" + + echo "" + prettyPrintCommand "parseinvoice" "paymentRequest" ";Parse a lightning payment request into human readable data." + + echo "" + prettyPrintCommand "findroute" "paymentRequest" ";Evaluate a route for a payment." + prettyPrintCommand "findroute" "paymentRequest" "amountMsat" + prettyPrintCommand "findroute" "nodeId" "amountMsat" + + prettyPrintCommand "checkpayment" "paymentHash" ";Check if a payment has been received." + prettyPrintCommand "checkpayment" "paymentRequest" + + echo "" + prettyPrintCommand "audit" ";List all send/received/relayed payments. Can filter with timestamps (in milliseconds)" + prettyPrintCommand "audit" "from" "to" + + echo "" + prettyPrintCommand "networkfees" ";List all network fees paid to the miners, by transaction. Can filter with timestamps (in milliseconds)" + prettyPrintCommand "networkfees" "from" "to" + exit 0; } From e663dbcc63f39ecec7adec577aa01b242e9c1315 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 25 Mar 2019 11:36:01 +0100 Subject: [PATCH 64/75] Use a mock Eclair in ApiServiceSpec --- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 121 +++++++++--------- 1 file changed, 58 insertions(+), 63 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 5164bc8584..acadb08dd2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -33,14 +33,44 @@ import akka.stream.ActorMaterializer import fr.acinq.eclair.channel.Register.ForwardShortId import org.json4s.{Formats, JValue} import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} +import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.eclair.channel.RES_GETINFO +import fr.acinq.eclair.db.{NetworkFee, Stats} import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} +import fr.acinq.eclair.router.{ChannelDesc, RouteResponse} +import fr.acinq.eclair.wire.{ChannelUpdate, NodeAddress, NodeAnnouncement} +import scodec.bits.ByteVector +import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source import scala.reflect.ClassTag class ApiServiceSpec extends FunSuite with ScalatestRouteTest { + trait EclairMock extends Eclair { + override def connect(uri: String): Future[String] = ??? + override def open(nodeId: Crypto.PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = ??? + override def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] = ??? + override def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId]): Future[String] = ??? + override def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] = ??? + override def peersInfo(): Future[Iterable[PeerInfo]] = ??? + override def channelsInfo(toRemoteNode: Option[Crypto.PublicKey]): Future[Iterable[RES_GETINFO]] = ??? + override def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] = ??? + override def allnodes(): Future[Iterable[NodeAnnouncement]] = ??? + override def allchannels(): Future[Iterable[ChannelDesc]] = ??? + override def allupdates(nodeId: Option[Crypto.PublicKey]): Future[Iterable[ChannelUpdate]] = ??? + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = ??? + override def findRoute(targetNodeId: Crypto.PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]]): Future[RouteResponse] = ??? + override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[PaymentLifecycle.PaymentResult] = ??? + override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = ??? + override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = ??? + override def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] = ??? + override def channelStats(): Future[Seq[Stats]] = ??? + override def getInfoResponse(): Future[GetInfoResponse] = ??? + } + implicit val formats = JsonSupport.formats implicit val serialization = JsonSupport.serialization implicit val marshaller = JsonSupport.marshaller @@ -48,35 +78,15 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { implicit val routeTestTimeout = RouteTestTimeout(3 seconds) - val defaultMockKit = Kit( - nodeParams = Alice.nodeParams, - system = system, - watcher = system.actorOf(Props(new MockActor)), - paymentHandler = system.actorOf(Props(new MockActor)), - register = system.actorOf(Props(new MockActor)), - relayer = system.actorOf(Props(new MockActor)), - router = system.actorOf(Props(new MockActor)), - switchboard = system.actorOf(Props(new MockActor)), - paymentInitiator = system.actorOf(Props(new MockActor)), - server = system.actorOf(Props(new MockActor)), - wallet = new TestWallet - ) - - class MockActor extends Actor { - override def receive: Receive = { - case _ => - } - } - - class MockService(kit: Kit = defaultMockKit) extends Service { - override val eclairApi: Eclair = new EclairImpl(kit) + class MockService(eclair: Eclair) extends Service { + override val eclairApi: Eclair = eclair override def password: String = "mock" override implicit val actorSystem: ActorSystem = system override implicit val mat: ActorMaterializer = materializer } test("API service should handle failures correctly") { - val mockService = new MockService + val mockService = new MockService(new EclairMock {}) // no auth Post("/getinfo") ~> @@ -129,34 +139,19 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { test("'peers' should ask the switchboard for current known peers") { - val mockAlicePeer = system.actorOf(Props(new {} with MockActor { - override def receive = { - case GetPeerInfo => sender() ! PeerInfo( + val mockService = new MockService(new EclairMock { + override def peersInfo(): Future[Iterable[PeerInfo]] = Future.successful(List( + PeerInfo( nodeId = Alice.nodeParams.nodeId, state = "CONNECTED", address = Some(Alice.nodeParams.publicAddresses.head.socketAddress), - channels = 1) - } - })) - - val mockBobPeer = system.actorOf(Props(new {} with MockActor { - override def receive = { - case GetPeerInfo => sender() ! PeerInfo( + channels = 1), + PeerInfo( nodeId = Bob.nodeParams.nodeId, state = "DISCONNECTED", address = None, - channels = 1) - } - })) - - - val mockService = new MockService(defaultMockKit.copy( - switchboard = system.actorOf(Props(new {} with MockActor { - override def receive = { - case 'peers => sender() ! List(mockAlicePeer, mockBobPeer) - } - })) - )) + channels = 1))) + }) Post("/peers") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> @@ -170,8 +165,16 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } test("'getinfo' response should include this node ID") { - Globals.blockCount.set(9999) - val mockService = new MockService() + + val mockService = new MockService(new EclairMock { + override def getInfoResponse(): Future[GetInfoResponse] = Future.successful(GetInfoResponse( + nodeId = Alice.nodeParams.nodeId, + alias = Alice.nodeParams.alias, + chainHash = Alice.nodeParams.chainHash, + blockHeight = 9999, + publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil + )) + }) Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> @@ -189,15 +192,11 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { val shortChannelIdSerialized = "42000x27x3" - val mockService = new MockService(defaultMockKit.copy( - register = system.actorOf(Props(new {} with MockActor { - override def receive = { - case ForwardShortId(shortChannelId, _) if shortChannelId.toString == shortChannelIdSerialized => - sender() ! Alice.nodeParams.nodeId.toString - } - })) - )) - + val mockService = new MockService(new EclairMock { + override def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] = { + Future.successful(Alice.nodeParams.nodeId.toString()) + } + }) Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> @@ -219,13 +218,9 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { val remotePort = "9735" val remoteUri = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735" - val mockService = new MockService(defaultMockKit.copy( - switchboard = system.actorOf(Props(new {} with MockActor { - override def receive = { - case Peer.Connect(_) => sender() ! "connected" - } - })) - )) + val mockService = new MockService( new EclairMock { + override def connect(uri: String): Future[String] = Future.successful("connected") + }) Post("/connect", FormData("nodeId" -> remoteNodeId, "host" -> remoteHost, "port" -> remotePort).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> From f3715fd90eceaa38159978e357f7fae1f44e73cb Mon Sep 17 00:00:00 2001 From: dpad85 Date: Mon, 25 Mar 2019 11:37:50 +0100 Subject: [PATCH 65/75] Removed help, we will instead point to an online documentation --- eclair-core/eclair-cli | 117 ++++++----------------------------------- 1 file changed, 16 insertions(+), 101 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 1cf48fdcda..0376158c1a 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -1,8 +1,9 @@ #!/bin/bash # default script values, can be overriden for convenience. -api_url='http://localhost:8081' -# api_password='your_api_password' # uncomment this if you don't want to provide a password each time you call eclair-cli +api_url='http://localhost:8080' +# uncomment the line below if you don't want to provide a password each time you call eclair-cli +# api_password='your_api_password' colors=false # prints help message @@ -10,6 +11,7 @@ usage() { echo -e "============================== Command line client for eclair ============================== + This tool requires the eclair node's API to be enabled and listening on <$api_url>. @@ -24,7 +26,7 @@ where OPTIONS can be: -h Show available commands and COMMAND is one of: - help, getinfo, connect, open, close, forceclose, updaterelayfee, + getinfo, connect, open, close, forceclose, updaterelayfee, peers, channels, channel, allnodes, allchannels, allupdates, receive, parseinvoice, findroute, findroutetonode, send, sendtonode, checkpayment, @@ -37,89 +39,8 @@ Examples eclair-cli close --channelId 006fb... closes the channel with id 006fb... -Full documentation at: " 1>&2; exit 1; -} - -# prints a pretty command doc. 1st arg is command, 2nd arg is params, 3rd arg is the description -prettyPrintCommand() { - method=${1} - params="" - desc="" - shift 1 - for arg in "${@}"; do - case $arg in - ";"*) desc="${arg:1}";; - *) params="$params--$arg ";; - esac - done; - if [[ ${#params} -gt 40 ]]; then - printf " %-50s \n%-54s %s\n" "$method $params" "" "$desc" - else - printf " %-50s %s\n" "$method $params" "$desc" - fi -} - -# prints the list of available commands with parameters -help() { - echo -e "\nAvailable commands:\n" - prettyPrintCommand "getinfo" ";Get node information" - echo "" - prettyPrintCommand "connect" "uri" ";Open a secure connection to a lightning node." - prettyPrintCommand "connect" "nodeId" "host" "port" - prettyPrintCommand "open" "nodeId" "fundingSatoshis" "pushMsat" "feerateSatPerByte" "channelFlags" ";Open a channel with another lightning node. Push, feerateSatPerByte and channelFlags are optional." - - echo "" - prettyPrintCommand "close" "channelId" ";Close channel." - prettyPrintCommand "close" "scriptPubKey" - prettyPrintCommand "forceclose" "channelId" ";Force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)." - - echo "" - prettyPrintCommand "peers" ";List nodes you have a channel with" - prettyPrintCommand "channels" ";List your local channels" - prettyPrintCommand "channels" "nodeId" ";List your local channels with a specific node" - prettyPrintCommand "channel" "channelId" ";Retrieve local channel detailed information." - - echo "" - prettyPrintCommand "updaterelayfee" "channelId" "feeBaseMsat" "feeProportionalMillionths" ";Update relay fee for payments going through this channel." - - echo "" - prettyPrintCommand "channelstats" ";Retrieve statistics about channel usage (fees, number and average amount of payments)." - - echo "" - prettyPrintCommand "allnodes" ";List all known nodes, channels, or updates in the network." - prettyPrintCommand "allchannels" - prettyPrintCommand "allupdates" - prettyPrintCommand "allupdates" "nodeId" - - echo "" - prettyPrintCommand "receive" "amountMsat" "description" ";Generate a Lightning payment request to receive funds." - prettyPrintCommand "receive" "amountMsat" "description" "expirySeconds" - - echo "" - prettyPrintCommand "send" "amountMsat" "paymentHash" "nodeId" ";Send a payment to a node, or with a Lightning payment request." - prettyPrintCommand "send" "paymentRequest" - prettyPrintCommand "send" "paymentRequest" "amountMsat" - - echo "" - prettyPrintCommand "parseinvoice" "paymentRequest" ";Parse a lightning payment request into human readable data." - - echo "" - prettyPrintCommand "findroute" "paymentRequest" ";Evaluate a route for a payment." - prettyPrintCommand "findroute" "paymentRequest" "amountMsat" - prettyPrintCommand "findroute" "nodeId" "amountMsat" - - prettyPrintCommand "checkpayment" "paymentHash" ";Check if a payment has been received." - prettyPrintCommand "checkpayment" "paymentRequest" - - echo "" - prettyPrintCommand "audit" ";List all send/received/relayed payments. Can filter with timestamps (in milliseconds)" - prettyPrintCommand "audit" "from" "to" - - echo "" - prettyPrintCommand "networkfees" ";List all network fees paid to the miners, by transaction. Can filter with timestamps (in milliseconds)" - prettyPrintCommand "networkfees" "from" "to" - - exit 0; +Full documentation here: " 1>&2; +exit 1; } # -- script's logic begins here @@ -136,7 +57,7 @@ while getopts ':cu:p:a:hu:' flag; do p) api_password="${OPTARG}" ;; a) api_url="${OPTARG}" ;; c) colors=true ;; - h) help ;; + h) usage ;; *) ;; esac done @@ -146,32 +67,26 @@ shift $(($OPTIND - 1)) api_endpoint=${1} shift 1 -# display a usage method if no method given -if [ -z $api_endpoint ]; then +# display a usage method if no method given or help requested +if [ -z $api_endpoint ] || [ "$api_endpoint" == "help" ]; then usage; fi -# display a help and exit -if [ "$api_endpoint" == "help" ]; then - help; -fi - # transform long options into a HTTP encoded url body. api_payload="" index=1 for arg in "${@}"; do transformed_arg="" case ${arg} in - "--"*) - # if arg begins with two dashes, it is the name of a parameter. Dashes must be removed, and arg must be followed by an equal sign + "--"*) # if arg begins with two dashes, it is the name of a parameter. Dashes must be removed, and arg must be followed by an equal sign # also, it must be prefixed by an '&' sign, if it is not the first argument - if [ $index -eq 1 ]; - then transformed_arg="$transformed_arg${arg:2}=" - else transformed_arg="&$transformed_arg${arg:2}=" + if [ $index -eq 1 ]; then + transformed_arg="$transformed_arg${arg:2}=" + else + transformed_arg="&$transformed_arg${arg:2}=" fi ;; - *) - transformed_arg=$arg + *) transformed_arg=$arg ;; esac api_payload="$api_payload$transformed_arg"; From 2f37cdc5e3a20b004ed36fd69db7d6dd057c311b Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 25 Mar 2019 11:41:48 +0100 Subject: [PATCH 66/75] Formatting --- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index acadb08dd2..56d947c762 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -51,23 +51,41 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { trait EclairMock extends Eclair { override def connect(uri: String): Future[String] = ??? + override def open(nodeId: Crypto.PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = ??? + override def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector]): Future[String] = ??? + override def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId]): Future[String] = ??? + override def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] = ??? + override def peersInfo(): Future[Iterable[PeerInfo]] = ??? + override def channelsInfo(toRemoteNode: Option[Crypto.PublicKey]): Future[Iterable[RES_GETINFO]] = ??? + override def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] = ??? + override def allnodes(): Future[Iterable[NodeAnnouncement]] = ??? + override def allchannels(): Future[Iterable[ChannelDesc]] = ??? + override def allupdates(nodeId: Option[Crypto.PublicKey]): Future[Iterable[ChannelUpdate]] = ??? + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = ??? + override def findRoute(targetNodeId: Crypto.PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]]): Future[RouteResponse] = ??? + override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[PaymentLifecycle.PaymentResult] = ??? + override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = ??? + override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = ??? + override def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] = ??? + override def channelStats(): Future[Seq[Stats]] = ??? + override def getInfoResponse(): Future[GetInfoResponse] = ??? } @@ -80,7 +98,9 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { class MockService(eclair: Eclair) extends Service { override val eclairApi: Eclair = eclair + override def password: String = "mock" + override implicit val actorSystem: ActorSystem = system override implicit val mat: ActorMaterializer = materializer } @@ -218,7 +238,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { val remotePort = "9735" val remoteUri = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735" - val mockService = new MockService( new EclairMock { + val mockService = new MockService(new EclairMock { override def connect(uri: String): Future[String] = Future.successful("connected") }) From 9c9afe479995dfc24f06b43165774fd3315eb96a Mon Sep 17 00:00:00 2001 From: dpad85 Date: Mon, 25 Mar 2019 18:39:17 +0100 Subject: [PATCH 67/75] Fixed eclair cli error parsing, added commands to eclair cli completion --- contrib/eclair-cli.bash-completion | 2 +- eclair-core/eclair-cli | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/eclair-cli.bash-completion b/contrib/eclair-cli.bash-completion index b88f979f6f..a5e846baf6 100644 --- a/contrib/eclair-cli.bash-completion +++ b/contrib/eclair-cli.bash-completion @@ -21,7 +21,7 @@ _eclair-cli() *) # works fine, but is too slow at the moment. # allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g') - allopts="connect open peers channels channel allnodes allchannels allupdates receive send close audit findroute updaterelayfee parseinvoice forceclose networkfees channelstats checkpayment getinfo help" + allopts="getinfo connect open close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates receive parseinvoice findroute findroutetonode send sendtonode checkpayment audit networkfees channelstats" if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 0376158c1a..46b18198bd 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -94,7 +94,7 @@ for arg in "${@}"; do done; # jq parses response body for error message -jq_filter='if .error == null then . else .error end' +jq_filter='if type=="object" and .error != null then .error else . end' # jq options if [ "$colors" = true ]; then From 0078d62e2765e77a90df025dc517918fe06cae0e Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 26 Mar 2019 12:56:37 +0100 Subject: [PATCH 68/75] Update README for the new docs and the new api, move old doc in a separate readme. --- OLD-API-DOCS.md | 40 ++++++++++++++++++++++++++++++++++++++++ README.md | 44 ++++++-------------------------------------- 2 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 OLD-API-DOCS.md diff --git a/OLD-API-DOCS.md b/OLD-API-DOCS.md new file mode 100644 index 0000000000..1c7f30abe6 --- /dev/null +++ b/OLD-API-DOCS.md @@ -0,0 +1,40 @@ + ## JSON-RPC API + + :warning: Note this interface is being deprecated. + + method | params | description + ------------- |----------------------------------------------------------------------------------------|----------------------------------------------------------- + getinfo | | return basic node information (id, chain hash, current block height) + connect | nodeId, host, port | open a secure connection to a lightning node + connect | uri | open a secure connection to a lightning node + open | nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced + updaterelayfee | channelId, feeBaseMsat, feeProportionalMillionths | update relay fee for payments going through this channel + peers | | list existing local peers + channels | | list existing local channels + channels | nodeId | list existing local channels opened with a particular nodeId + channel | channelId | retrieve detailed information about a given channel + channelstats | | retrieves statistics about channel usage (fees, number and average amount of payments) + allnodes | | list all known nodes + allchannels | | list all known channels + allupdates | | list all channels updates + allupdates | nodeId | list all channels updates for this nodeId + 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 + receive | amountMsat, description, expirySeconds | generate a payment request for a given amount that expires after given number of seconds + parseinvoice | paymentRequest | returns node, amount and payment hash in a payment request + findroute | paymentRequest | returns nodes and channels of the route for this payment request if there is any + findroute | paymentRequest, amountMsat | returns nodes and channels of the route for this payment request and amount, if there is any + findroute | nodeId, amountMsat | returns nodes and channels of the route to the nodeId, if there is any + 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 + send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount + checkpayment | paymentHash | returns true if the payment has been received, false otherwise + checkpayment | paymentRequest | returns true if the payment has been received, false otherwise + close | channelId | close a channel + close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey + forceclose | channelId | force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)" + audit | | list all send/received/relayed payments + audit | from, to | list send/received/relayed payments in that interval (from <= timestamp < to) + networkfees | | list all network fees paid to the miners, by transaction + networkfees |from, to | list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to) + help | | display available methods diff --git a/README.md b/README.md index 3cbf722082..f9cf76016b 100644 --- a/README.md +++ b/README.md @@ -128,44 +128,12 @@ Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a different java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gui--.jar ``` -## JSON-RPC API - - method | params | description - ------------- |----------------------------------------------------------------------------------------|----------------------------------------------------------- - getinfo | | return basic node information (id, chain hash, current block height) - connect | nodeId, host, port | open a secure connection to a lightning node - connect | uri | open a secure connection to a lightning node - open | nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced - updaterelayfee | channelId, feeBaseMsat, feeProportionalMillionths | update relay fee for payments going through this channel - peers | | list existing local peers - channels | | list existing local channels - channels | nodeId | list existing local channels opened with a particular nodeId - channel | channelId | retrieve detailed information about a given channel - channelstats | | retrieves statistics about channel usage (fees, number and average amount of payments) - allnodes | | list all known nodes - allchannels | | list all known channels - allupdates | | list all channels updates - allupdates | nodeId | list all channels updates for this nodeId - 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 - receive | amountMsat, description, expirySeconds | generate a payment request for a given amount that expires after given number of seconds - parseinvoice | paymentRequest | returns node, amount and payment hash in a payment request - findroute | paymentRequest | returns nodes and channels of the route for this payment request if there is any - findroute | paymentRequest, amountMsat | returns nodes and channels of the route for this payment request and amount, if there is any - findroute | nodeId, amountMsat | returns nodes and channels of the route to the nodeId, if there is any - 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 - send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount - checkpayment | paymentHash | returns true if the payment has been received, false otherwise - checkpayment | paymentRequest | returns true if the payment has been received, false otherwise - close | channelId | close a channel - close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey - forceclose | channelId | force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)" - audit | | list all send/received/relayed payments - audit | from, to | list send/received/relayed payments in that interval (from <= timestamp < to) - networkfees | | list all network fees paid to the miners, by transaction - networkfees |from, to | list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to) - help | | display available methods +## JSON API + +Eclair offers a feature rich HTTP API that enables application developers to easily integrate, for the full documentation please visit +the [website](https://acinq.github.io/eclair). If you are still using the old APIs and looking for documentation you can find it [here](https://github.com/ACINQ/eclair/OLD-API-DOCS.md) +but is not maintained anymore. + ## Docker From 976cf163a0a0a34aa4b2155b7e07a152d80836ed Mon Sep 17 00:00:00 2001 From: dpad85 Date: Tue, 26 Mar 2019 13:18:45 +0100 Subject: [PATCH 69/75] Added short options to eclair-cli, and removed color option Short options adds a jq filter for some methods such as channels or channel in order to ouput a smaller and more readable json. Color option has been removed because it adds ansi special characters which breaks jq piping on some systems. --- eclair-core/eclair-cli | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 46b18198bd..f002c95317 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -4,7 +4,8 @@ api_url='http://localhost:8080' # uncomment the line below if you don't want to provide a password each time you call eclair-cli # api_password='your_api_password' -colors=false +# for some commands the json output can be shortened for better readability +short=false # prints help message usage() { @@ -22,7 +23,6 @@ Usage where OPTIONS can be: -p API's password -a
Override the API URL with
- -c Outputs colored JSON -h Show available commands and COMMAND is one of: @@ -52,12 +52,12 @@ command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installa command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborting..."; exit 1; } # extract script options -while getopts ':cu:p:a:hu:' flag; do +while getopts ':cu:su:p:a:hu:' flag; do case "${flag}" in p) api_password="${OPTARG}" ;; a) api_url="${OPTARG}" ;; - c) colors=true ;; h) usage ;; + s) short=true ;; *) ;; esac done @@ -76,14 +76,14 @@ fi api_payload="" index=1 for arg in "${@}"; do - transformed_arg="" + transformed_arg=""; case ${arg} in "--"*) # if arg begins with two dashes, it is the name of a parameter. Dashes must be removed, and arg must be followed by an equal sign # also, it must be prefixed by an '&' sign, if it is not the first argument if [ $index -eq 1 ]; then - transformed_arg="$transformed_arg${arg:2}=" + transformed_arg="$transformed_arg${arg:2}="; else - transformed_arg="&$transformed_arg${arg:2}=" + transformed_arg="&$transformed_arg${arg:2}="; fi ;; *) transformed_arg=$arg @@ -93,16 +93,21 @@ for arg in "${@}"; do let "index++" done; -# jq parses response body for error message -jq_filter='if type=="object" and .error != null then .error else . end' +# jq filter parses response body for error message +jq_filter='if type=="object" and .error != null then .error else .'; -# jq options -if [ "$colors" = true ]; then - jq_opts="--color-output"; -else - jq_opts="--monochrome-output" +# apply special jq filter if we are in "short" ouput mode -- only for specific commands such as 'channels' +if [ "$short" = true ]; then + jq_channel_filter="{ nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }"; + case $api_endpoint in + "channels") jq_filter="$jq_filter | map( $jq_channel_filter )" ;; + "channel") jq_filter="$jq_filter | $jq_channel_filter" ;; + *) ;; + esac fi +jq_filter="$jq_filter end"; + # if no password is provided, auth should only contain user login so that curl prompts for the api password if [ -z $api_password ]; then auth="eclair-cli"; @@ -111,4 +116,4 @@ else fi # we're now ready to execute the API call -eval curl "--user $auth --silent --show-error -X POST -H \"Content-Type: application/x-www-form-urlencoded\" -d '$api_payload' $api_url/$api_endpoint" | jq -r "$jq_opts" "$jq_filter" +eval curl "--user $auth --silent --show-error -X POST -H \"Content-Type: application/x-www-form-urlencoded\" -d '$api_payload' $api_url/$api_endpoint" | jq -r "$jq_filter" From 97e1d897f60c23d1702488187b172820eec10d31 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 26 Mar 2019 15:43:26 +0100 Subject: [PATCH 70/75] Add instructions to build/update the doc --- BUILD.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/BUILD.md b/BUILD.md index 753ecccb6a..92f955dff4 100644 --- a/BUILD.md +++ b/BUILD.md @@ -24,3 +24,16 @@ To only build the `eclair-node` module $ mvn install -pl eclair-node -am -DskipTests ``` +# Building the API documentation + +## Slate + +The API doc is generated via slate and hosted on github pages. To make a change and update the doc follow the steps: + +1. git checkout slate-doc +2. Install your local dependencies for slate, more info [here](https://github.com/lord/slate#getting-started-with-slate) +3. Edit `source/index.html.md` and save your changes. +4. Commit all the changes to git, before deploying the repo should be clean. +5. Push your commit to remote. +6. Run `./deploy.sh` +7. Wait a few minutes and the doc should be updated at https://acinq.github.io/eclair \ No newline at end of file From ba6361452ae90d2b02b037b4f6b79fd499d387c1 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 26 Mar 2019 16:06:40 +0100 Subject: [PATCH 71/75] Remove writing-to-file part of the regression test for the API --- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 56d947c762..08250495a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -16,36 +16,29 @@ package fr.acinq.eclair.api - -import java.nio.file.{Files, Path, Paths, StandardOpenOption} - import akka.actor.{Actor, ActorSystem, Props, Scheduler} import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} -import fr.acinq.eclair.blockchain.TestWallet import fr.acinq.eclair._ import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import TestConstants._ import akka.http.scaladsl.model.headers.BasicHttpCredentials import akka.http.scaladsl.server.Route import akka.stream.ActorMaterializer -import fr.acinq.eclair.channel.Register.ForwardShortId -import org.json4s.{Formats, JValue} import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.channel.RES_GETINFO import fr.acinq.eclair.db.{NetworkFee, Stats} -import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} import fr.acinq.eclair.router.{ChannelDesc, RouteResponse} import fr.acinq.eclair.wire.{ChannelUpdate, NodeAddress, NodeAnnouncement} import scodec.bits.ByteVector - import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source import scala.reflect.ClassTag +import scala.util.Try class ApiServiceSpec extends FunSuite with ScalatestRouteTest { @@ -180,7 +173,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(handled) assert(status == OK) val response = entityAs[String] - matchTestJson("peers", false, response) + matchTestJson("peers", response) } } @@ -204,7 +197,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(status == OK) val resp = entityAs[String] assert(resp.toString.contains(Alice.nodeParams.nodeId.toString)) - matchTestJson("getinfo", false, resp) + matchTestJson("getinfo", resp) } } @@ -227,7 +220,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(status == OK) val resp = entityAs[String] assert(resp.contains(Alice.nodeParams.nodeId.toString)) - matchTestJson("close", false, resp) + matchTestJson("close", resp) } } @@ -262,16 +255,12 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } } - private def matchTestJson(apiName: String, overWrite: Boolean, response: String) = { - val p = Paths.get(s"src/test/resources/api/$apiName") - - if (overWrite) { - Files.writeString(p, response) - assert(false, "'overWrite' should be false before commit") - } else { - val expectedResponse = Source.fromFile(p.toUri).mkString - assert(response == expectedResponse, s"Test mock for $apiName did not match the expected response") + private def matchTestJson(apiName: String, response: String) = { + val resource = getClass.getResourceAsStream(s"/api/$apiName") + val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse { + throw new IllegalArgumentException(s"Mock file for $apiName not found") } + assert(response == expectedResponse, s"Test mock for $apiName did not match the expected response") } } \ No newline at end of file From 23c3515e8da6aae7819903cae81e0ab8ec667480 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 26 Mar 2019 16:11:37 +0100 Subject: [PATCH 72/75] Make 'port' optional when using 'connect' API --- .../src/main/scala/fr/acinq/eclair/api/Service.scala | 6 ++++-- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 3003b1fa21..1674096fb4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -15,9 +15,11 @@ import akka.http.scaladsl.model.ws.{Message, TextMessage} import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} +import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} import grizzled.slf4j.Logging import scodec.bits.ByteVector + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -99,8 +101,8 @@ trait Service extends Directives with Logging { path("connect") { formFields("uri".as[String]) { uri => complete(eclairApi.connect(uri)) - } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int]) { (nodeId, host, port) => - complete(eclairApi.connect(s"$nodeId@$host:$port")) + } ~ formFields("nodeId".as[PublicKey], "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => + complete(eclairApi.connect(s"$nodeId@$host:${port_opt.getOrElse(NodeURI.DEFAULT_PORT)}")) } } ~ path("open") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 08250495a0..58c1b41c52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -228,14 +228,13 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { val remoteNodeId = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" val remoteHost = "93.137.102.239" - val remotePort = "9735" val remoteUri = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735" val mockService = new MockService(new EclairMock { override def connect(uri: String): Future[String] = Future.successful("connected") }) - Post("/connect", FormData("nodeId" -> remoteNodeId, "host" -> remoteHost, "port" -> remotePort).toEntity) ~> + Post("/connect", FormData("nodeId" -> remoteNodeId, "host" -> remoteHost).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> check { From be8692771dc0a4806b8a99e5f695d2e3a81e7911 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 26 Mar 2019 16:24:20 +0100 Subject: [PATCH 73/75] Merge with master --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 10 +++++----- .../main/scala/fr/acinq/eclair/api/OldService.scala | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index a7f32f2c09..729e00f070 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -154,9 +154,9 @@ class EclairImpl(appKit: Kit) extends Eclair { } Future(AuditResponse( - sent = appKit.nodeParams.auditDb.listSent(from, to), - received = appKit.nodeParams.auditDb.listReceived(from, to), - relayed = appKit.nodeParams.auditDb.listRelayed(from, to) + sent = appKit.nodeParams.db.audit.listSent(from, to), + received = appKit.nodeParams.db.audit.listReceived(from, to), + relayed = appKit.nodeParams.db.audit.listRelayed(from, to) )) } @@ -166,10 +166,10 @@ class EclairImpl(appKit: Kit) extends Eclair { case _ => (0L, Long.MaxValue) } - Future(appKit.nodeParams.auditDb.listNetworkFees(from, to)) + Future(appKit.nodeParams.db.audit.listNetworkFees(from, to)) } - override def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.auditDb.stats) + override def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.db.audit.stats) /** * Sends a request to a channel and expects a response diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala index 4d6fd03836..13bb29889e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -321,9 +321,9 @@ trait OldService extends Logging { case _ => (0L, Long.MaxValue) } completeRpcFuture(req.id, Future(AuditResponse( - sent = nodeParams.auditDb.listSent(from, to), - received = nodeParams.auditDb.listReceived(from, to), - relayed = nodeParams.auditDb.listRelayed(from, to)) + sent = nodeParams.db.audit.listSent(from, to), + received = nodeParams.db.audit.listReceived(from, to), + relayed = nodeParams.db.audit.listRelayed(from, to)) )) case "networkfees" => @@ -331,10 +331,10 @@ trait OldService extends Logging { case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong) case _ => (0L, Long.MaxValue) } - completeRpcFuture(req.id, Future(nodeParams.auditDb.listNetworkFees(from, to))) + completeRpcFuture(req.id, Future(nodeParams.db.audit.listNetworkFees(from, to))) // retrieve fee stats - case "channelstats" => completeRpcFuture(req.id, Future(nodeParams.auditDb.stats)) + case "channelstats" => completeRpcFuture(req.id, Future(nodeParams.db.audit.stats)) // method name was not found From a8070bb6d64cf1f1b573ffabfdcd84a9dffd56a8 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 26 Mar 2019 16:39:04 +0100 Subject: [PATCH 74/75] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f9cf76016b..b826a8316c 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,11 @@ java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gu ## JSON API -Eclair offers a feature rich HTTP API that enables application developers to easily integrate, for the full documentation please visit -the [website](https://acinq.github.io/eclair). If you are still using the old APIs and looking for documentation you can find it [here](https://github.com/ACINQ/eclair/OLD-API-DOCS.md) -but is not maintained anymore. +Eclair offers a feature rich HTTP API that enables application developers to easily integrate. + +For more information please visit the [API documentation website](https://acinq.github.io/eclair). + +:warning: You can still use the old API by setting the `eclair.api.use-old-api=true` parameter, but it is now deprecated and will soon be removed. The old documentation is still available [here](https://github.com/ACINQ/eclair/OLD-API-DOCS.md). ## Docker From 7a94d3c8ed184c3eb6aa61720054fd5f08d49a42 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 26 Mar 2019 16:40:17 +0100 Subject: [PATCH 75/75] Correct JSON-RPC to JSON in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9cf76016b..d9c61e8259 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Gitter chat](https://img.shields.io/badge/chat-on%20gitter-red.svg)](https://gitter.im/ACINQ/eclair) -**Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON-RPC API is also available. +**Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON API is also available. This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning) and [lnd](https://github.com/LightningNetwork/lnd). @@ -14,7 +14,7 @@ This software follows the [Lightning Network Specifications (BOLTs)](https://git :rotating_light: If you intend to run Eclair on mainnet: - Keep in mind that it is beta-quality software and **don't put too much money** in it - - Eclair's JSON-RPC API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API) + - Eclair's JSON API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API) - Specific [configuration instructions for mainnet](#mainnet-usage) are provided below (by default Eclair runs on testnet) ---