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
10 changes: 10 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Expand Up @@ -26,6 +26,7 @@ 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.blockchain.bitcoind.BitcoinCoreWallet.WalletTransaction
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats}
Expand Down Expand Up @@ -127,6 +128,8 @@ trait Eclair {

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

def listTransactions(count: Int, skip: Int)(implicit timeout: Timeout): Future[Iterable[WalletTransaction]]
t-bast marked this conversation as resolved.
Show resolved Hide resolved

}

class EclairImpl(appKit: Kit) extends Eclair {
Expand Down Expand Up @@ -225,6 +228,13 @@ class EclairImpl(appKit: Kit) extends Eclair {
}
}

override def listTransactions(count: Int, skip: Int)(implicit timeout: Timeout): Future[Iterable[WalletTransaction]] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.listTransactions(count, skip)
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 @@ -44,7 +44,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDecimal(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue, Satoshi(fee.bigDecimal.scaleByPowerOfTen(8).longValue))
FundTransactionResponse(Transaction.read(hex), changepos.intValue, toSatoshi(fee))
})
}

Expand All @@ -65,6 +65,28 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC

def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = bitcoinClient.publishTransaction(tx)

def listTransactions(count: Int, skip: Int): Future[List[WalletTransaction]] = rpcClient.invoke("listtransactions", "*", count, skip).map({
case JArray(txs) => txs.map(tx => {
val JString(address) = tx \ "address"
val JDecimal(amount) = tx \ "amount"
// fee is optional and only included for sent transactions
val fee = tx \ "fee" match {
case JDecimal(fee) => toSatoshi(fee)
case _ => Satoshi(0)
}
val JInt(confirmations) = tx \ "confirmations"
// while transactions are still in the mempool, block hash will no be included
val blockHash = tx \ "blockhash" match {
case JString(blockHash) => ByteVector32.fromValidHex(blockHash)
case _ => ByteVector32.Zeroes
}
val JString(txid) = tx \ "txid"
val JInt(timestamp) = tx \ "time"
WalletTransaction(address, toSatoshi(amount), fee, blockHash, confirmations.toLong, ByteVector32.fromValidHex(txid), timestamp.toLong)
}).reverse
case _ => Nil
})

/**
*
* @param outPoints outpoints to unlock
Expand All @@ -91,8 +113,6 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
}

override def getBalance: Future[OnChainBalance] = rpcClient.invoke("getbalances").map(json => {
def toSatoshi(v: BigDecimal): Satoshi = Satoshi(v.bigDecimal.scaleByPowerOfTen(8).longValue)

val JDecimal(confirmed) = json \ "mine" \ "trusted"
val JDecimal(unconfirmed) = json \ "mine" \ "untrusted_pending"
OnChainBalance(toSatoshi(confirmed), toSatoshi(unconfirmed))
Expand Down Expand Up @@ -180,8 +200,11 @@ object BitcoinCoreWallet {
// @formatter:off
case class Options(lockUnspents: Boolean, feeRate: BigDecimal)
case class Utxo(txid: ByteVector32, vout: Long)
case class WalletTransaction(address: String, amount: Satoshi, fees: Satoshi, blockHash: ByteVector32, confirmations: Long, txid: ByteVector32, timestamp: Long)
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Satoshi)
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
// @formatter:on

private def toSatoshi(amount: BigDecimal): Satoshi = Satoshi(amount.bigDecimal.scaleByPowerOfTen(8).longValue)

}
Expand Up @@ -23,7 +23,7 @@ import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, Btc, ByteVector32, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse}
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse, WalletTransaction}
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError}
import fr.acinq.eclair.transactions.Scripts
Expand Down Expand Up @@ -370,6 +370,12 @@ class BitcoinCoreWalletSpec extends TestKitBaseClass with BitcoindService with A
sender.expectMsg(false)
// let's confirm tx2
generateBlocks(bitcoincli, 1)
wallet.listTransactions(25, 0).pipeTo(sender.ref)
val Some(tx) = sender.expectMsgType[List[WalletTransaction]].collectFirst { case tx if tx.address == address => tx }
assert(tx.amount < 0.sat)
assert(tx.fees < 0.sat)
assert(tx.confirmations === 1)
assert(tx.txid === tx2.txid)
// this time tx1 has been double spent
wallet.doubleSpent(tx1).pipeTo(sender.ref)
sender.expectMsg(true)
Expand Down
5 changes: 5 additions & 0 deletions eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala
Expand Up @@ -288,6 +288,11 @@ trait Service extends ExtraDirectives with Logging {
} ~
path("getnewaddress") {
complete(eclairApi.newAddress())
} ~
path("listtransactions") {
formFields("count".as[Int].?, "skip".as[Int].?) { (count_opt, skip_opt) =>
complete(eclairApi.listTransactions(count_opt.getOrElse(10), skip_opt.getOrElse(0)))
}
}
} ~ get {
path("ws") {
Expand Down