Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add on-chain APIs #1461

Merged
merged 7 commits into from Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 13 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Expand Up @@ -24,6 +24,7 @@ import akka.util.Timeout
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.TimestampQueryFilters._
import fr.acinq.eclair.blockchain.OnChainBalance
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
import fr.acinq.eclair.channel._
Expand Down Expand Up @@ -88,7 +89,7 @@ trait Eclair {

def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest]

def newAddress(): Future[String]
def newAddress()(implicit timeout: Timeout): Future[String]
t-bast marked this conversation as resolved.
Show resolved Hide resolved

def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]]

Expand Down Expand Up @@ -123,6 +124,9 @@ trait Eclair {
def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse]

def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]]

def onChainBalance()(implicit timeout: Timeout): Future[OnChainBalance]

}

class EclairImpl(appKit: Kit) extends Eclair {
Expand Down Expand Up @@ -207,13 +211,20 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest]
}

override def newAddress(): Future[String] = {
override def newAddress()(implicit timeout: Timeout): Future[String] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.getReceiveAddress
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}

override def onChainBalance()(implicit timeout: Timeout): Future[OnChainBalance] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.getBalance
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}

override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = {
val maxFee = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf).getMaxFee(amount)
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, maxFee, assistedRoutes)).mapTo[RouteResponse]
Expand Down
Expand Up @@ -16,18 +16,18 @@

package fr.acinq.eclair.blockchain

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Satoshi, Transaction}
import scodec.bits.ByteVector

import scala.concurrent.Future

/**
* Created by PM on 06/07/2017.
*/
* Created by PM on 06/07/2017.
*/
trait EclairWallet {

def getBalance: Future[Satoshi]
def getBalance: Future[OnChainBalance]

def getReceiveAddress: Future[String]

Expand All @@ -36,37 +36,31 @@ trait EclairWallet {
def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]

/**
* Committing *must* include publishing the transaction on the network.
*
* We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the
* funding tx won't end up on the blockchain: if that happens and we have cancelled the channel, then we would lose our
* funds!
*
* @param tx
* @return true if success
* false IF AND ONLY IF *HAS NOT BEEN PUBLISHED* otherwise funds are at risk!!!
*/
* Committing *must* include publishing the transaction on the network.
*
* We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the
* funding tx won't end up on the blockchain: if that happens and we have cancelled the channel, then we would lose our
* funds!
*
* @return true if success
* false IF AND ONLY IF *HAS NOT BEEN PUBLISHED* otherwise funds are at risk!!!
*/
def commit(tx: Transaction): Future[Boolean]

/**
* Cancels this transaction: this probably translates to "release locks on utxos".
*
* @param tx
* @return
*/
* Cancels this transaction: this probably translates to "release locks on utxos".
*/
def rollback(tx: Transaction): Future[Boolean]


/**
* Tests whether the inputs of the provided transaction have been spent by another transaction.
*
* Implementations may always return false if they don't want to implement it
*
* @param tx
* @return
*/
* Tests whether the inputs of the provided transaction have been spent by another transaction.
*
* Implementations may always return false if they don't want to implement it
*/
def doubleSpent(tx: Transaction): Future[Boolean]

}

final case class OnChainBalance(confirmed: Satoshi, unconfirmed: Satoshi)

final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)
Expand Up @@ -16,7 +16,7 @@

package fr.acinq.eclair.blockchain.bitcoind

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
Expand Down Expand Up @@ -90,7 +90,13 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
future.map(_.forall(b => b))
}

override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDecimal(balance) => Satoshi(balance.bigDecimal.scaleByPowerOfTen(8).longValue) }
override def getBalance: Future[OnChainBalance] = rpcClient.invoke("getbalances").map(json => {
def toSatoshi(v: BigDecimal): Satoshi = Satoshi(v.bigDecimal.scaleByPowerOfTen(8).longValue)
t-bast marked this conversation as resolved.
Show resolved Hide resolved

val JDecimal(confirmed) = json \ "mine" \ "trusted"
val JDecimal(unconfirmed) = json \ "mine" \ "untrusted_pending"
OnChainBalance(toSatoshi(confirmed), toSatoshi(unconfirmed))
})

override def getReceiveAddress: Future[String] = for {
JString(address) <- rpcClient.invoke("getnewaddress")
Expand Down Expand Up @@ -139,7 +145,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
logger.warn(s"txid=${tx.txid} error=$e")
bitcoinClient.getTransaction(tx.txid).transformWith {
case Success(_) => Future.successful(true) // tx is in the mempool, we consider that it was published
case Failure(_) => rollback(tx).transform { case _ => Success(false) } // we use transform here because we want to return false in all cases even if rollback fails
case Failure(_) => rollback(tx).transform(_ => Success(false)) // we use transform here because we want to return false in all cases even if rollback fails
}
}

Expand Down
Expand Up @@ -22,28 +22,28 @@ import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, Transaction, TxO
import fr.acinq.eclair.addressToPublicKeyScript
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.BroadcastTransaction
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse}
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse, OnChainBalance}
import grizzled.slf4j.Logging
import scodec.bits.ByteVector

import scala.concurrent.{ExecutionContext, Future}

class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implicit system: ActorSystem, ec: ExecutionContext, timeout: akka.util.Timeout) extends EclairWallet with Logging {

override def getBalance = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => balance.confirmed + balance.unconfirmed)
override def getBalance: Future[OnChainBalance] = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => OnChainBalance(balance.confirmed, balance.unconfirmed))

override def getReceiveAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)
override def getReceiveAddress: Future[String] = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)

override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.failed(new RuntimeException("Not implemented"))

def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse]

override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0)
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match {
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map {
case CompleteTransactionResponse(tx1, fee1, None) => MakeFundingTxResponse(tx1, 0, fee1)
case CompleteTransactionResponse(_, _, Some(error)) => throw error
})
}
}

override def commit(tx: Transaction): Future[Boolean] =
Expand All @@ -70,7 +70,6 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implic
def sendPayment(amount: Satoshi, address: String, feeRatePerKw: Long): Future[String] = {
val publicKeyScript = Script.write(addressToPublicKeyScript(address, chainHash))
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, publicKeyScript) :: Nil, lockTime = 0)

(wallet ? CompleteTransaction(tx, feeRatePerKw))
.mapTo[CompleteTransactionResponse]
.flatMap {
Expand All @@ -96,4 +95,5 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implic
override def doubleSpent(tx: Transaction): Future[Boolean] = {
(wallet ? IsDoubleSpent(tx)).mapTo[IsDoubleSpentResponse].map(_.isDoubleSpent)
}

}
Expand Up @@ -16,10 +16,9 @@

package fr.acinq.eclair.blockchain

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, ByteVector32, Crypto, OutPoint, Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.eclair.LongToBtcAmount
import scodec.bits.ByteVector
import scodec.bits._

import scala.concurrent.Future
Expand All @@ -31,14 +30,14 @@ class TestWallet extends EclairWallet {

var rolledback = Set.empty[Transaction]

override def getBalance: Future[Satoshi] = ???
override def getBalance: Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat))

override def getReceiveAddress: Future[String] = Future.successful("bcrt1qwcv8naajwn8fjhu8z59q9e6ucrqr068rlcenux")

override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.successful(PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12"))

override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount, feeRatePerKw))
Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount))

override def commit(tx: Transaction): Future[Boolean] = Future.successful(true)

Expand All @@ -52,11 +51,12 @@ class TestWallet extends EclairWallet {

object TestWallet {

def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): MakeFundingTxResponse = {
def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = {
val fundingTx = Transaction(version = 2,
txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
MakeFundingTxResponse(fundingTx, 0, 420 sat)
}

}