Skip to content

Commit

Permalink
Refactor bitcoin client's fund psbt method
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Aug 24, 2021
1 parent 3bdc973 commit 6b2acab
Show file tree
Hide file tree
Showing 6 changed files with 22 additions and 18 deletions.
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ class Setup(val datadir: File,
routerTimeout = after(FiniteDuration(config.getDuration("router.init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out")))
_ <- Future.firstCompletedOf(routerInitialized.future :: routerTimeout :: Nil)

wallet = new BitcoinCoreWallet(Block.RegtestGenesisBlock.hash, bitcoin)
wallet = new BitcoinCoreWallet(nodeParams.chainHash, bitcoin)
_ = wallet.getReceiveAddress().map(address => logger.info(s"initial wallet address=$address"))

channelsListener = system.spawn(ChannelsListener(channelsListenerReady), name = "channels-listener")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class BitcoinCoreWallet(chainHash: ByteVector32, rpcClient: BitcoinJsonRPCClient
})
}

def fundTransaction(outputs: Map[String, Satoshi], lockUtxos: Boolean, feeRatePerKw: FeeratePerKw): Future[FundTransactionResponse] = {
def fundTransaction(outputs: Seq[(String, Satoshi)], lockUtxos: Boolean, feeRatePerKw: FeeratePerKw): Future[FundTransactionResponse] = {
val requestedFeeRatePerKB = FeeratePerKB(feeRatePerKw)
rpcClient.invoke("getmempoolinfo").map(json => json \ "mempoolminfee" match {
case JDecimal(feerate) => FeeratePerKB(Btc(feerate).toSatoshi).max(requestedFeeRatePerKB)
Expand All @@ -70,7 +70,7 @@ class BitcoinCoreWallet(chainHash: ByteVector32, rpcClient: BitcoinJsonRPCClient
})
}

def fundPsbt(outputs: Map[String, Satoshi], lockUtxos: Boolean, feeRatePerKw: FeeratePerKw): Future[FundPsbtResponse] = {
def fundPsbt(outputs: Seq[(String, Satoshi)], lockUtxos: Boolean, feeRatePerKw: FeeratePerKw): Future[FundPsbtResponse] = {
val requestedFeeRatePerKB = FeeratePerKB(feeRatePerKw)
rpcClient.invoke("getmempoolinfo").map(json => json \ "mempoolminfee" match {
case JDecimal(feerate) => FeeratePerKB(Btc(feerate).toSatoshi).max(requestedFeeRatePerKB)
Expand Down Expand Up @@ -198,8 +198,9 @@ class BitcoinCoreWallet(chainHash: ByteVector32, rpcClient: BitcoinJsonRPCClient
for {
// we ask bitcoin core to create and fund the funding tx
actualFeeRate <- getMinFeerate(feeRatePerKw)
FundPsbtResponse(psbt, fee, Some(changePos)) <- bitcoinClient.fundPsbt(Map(fundingAddress -> amount), 0, FundPsbtOptions(FeeratePerKw(actualFeeRate), lockUtxos = true))
FundPsbtResponse(psbt, fee, Some(changePos)) <- bitcoinClient.fundPsbt(Seq(fundingAddress -> amount), 0, FundPsbtOptions(FeeratePerKw(actualFeeRate), lockUtxos = true))
ourbip32path = localFundingKey.path.drop(2)
_ = logger.info(s"funded psbt = $psbt")
output = psbt.outputs(1 - changePos).copy(
derivationPaths = Map(
localFundingKey.publicKey -> Psbt.KeyPathWithMaster(localFundingKey.parent, ourbip32path),
Expand All @@ -208,8 +209,11 @@ class BitcoinCoreWallet(chainHash: ByteVector32, rpcClient: BitcoinJsonRPCClient
)
psbt1 = psbt.copy(outputs = psbt.outputs.updated(1 - changePos, output))
// now let's sign the funding tx
ProcessPsbtResponse(signedPsbt, true) <- signPsbtOrUnlock(psbt1)
Success(fundingTx) = signedPsbt.extract()
ProcessPsbtResponse(signedPsbt, complete) <- signPsbtOrUnlock(psbt1)
_ = logger.info(s"psbt signing complete = $complete")
extracted = signedPsbt.extract()
_ = if (extracted.isFailure) logger.error(s"psbt failure $extracted")
fundingTx = extracted.get
// there will probably be a change output, so we need to find which output is ours
outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, fundingPubkeyScript) match {
case Right(outputIndex) => Future.successful(outputIndex)
Expand All @@ -234,7 +238,7 @@ class BitcoinCoreWallet(chainHash: ByteVector32, rpcClient: BitcoinJsonRPCClient
for {
// we ask bitcoin core to create and fund the funding tx
actualFeeRate <- getMinFeerate(feerate)
FundPsbtResponse(psbt, fee, changePosition) <- bitcoinClient.fundPsbt(Map(fundingAddress -> amount), 0, FundPsbtOptions(FeeratePerKw(actualFeeRate), lockUtxos = true))
FundPsbtResponse(psbt, fee, changePosition) <- bitcoinClient.fundPsbt(Seq(fundingAddress -> amount), 0, FundPsbtOptions(FeeratePerKw(actualFeeRate), lockUtxos = true))
// now let's sign the funding tx
ProcessPsbtResponse(signedPsbt, true) <- signPsbtOrUnlock(psbt)
Success(fundingTx) = signedPsbt.extract()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) extends Logging
})
}

def fundPsbt(inputs: Seq[FundPsbtInput], outputs: Map[String, Satoshi], locktime: Int, options: FundPsbtOptions)(implicit ec: ExecutionContext): Future[FundPsbtResponse] = {
rpcClient.invoke("walletcreatefundedpsbt", inputs.toArray, outputs.map { case (a,b) => a -> b.toBtc.toBigDecimal }, locktime, options).map(json => {
def fundPsbt(inputs: Seq[FundPsbtInput], outputs: Seq[(String, Satoshi)], locktime: Long, options: FundPsbtOptions)(implicit ec: ExecutionContext): Future[FundPsbtResponse] = {
rpcClient.invoke("walletcreatefundedpsbt", inputs.toArray, outputs.map { case (a, b) => a -> b.toBtc.toBigDecimal }, locktime, options).map(json => {
val JString(base64) = json \ "psbt"
val JInt(changePos) = json \ "changepos"
val JDecimal(fee) = json \ "fee"
Expand All @@ -113,7 +113,7 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) extends Logging
})
}

def fundPsbt(outputs: Map[String, Satoshi], locktime: Int, options: FundPsbtOptions)(implicit ec: ExecutionContext): Future[FundPsbtResponse] =
def fundPsbt(outputs: Seq[(String, Satoshi)], locktime: Long, options: FundPsbtOptions)(implicit ec: ExecutionContext): Future[FundPsbtResponse] =
fundPsbt(Seq(), outputs, locktime, options)

def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Int = SIGHASH_ALL)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ExtendedBitcoinClientSpec extends TestKitBaseClass with BitcoindService wi
val script = Script.createMultiSigMofN(2, Seq(priv1.publicKey, priv2.publicKey))
val address = Bech32.encodeWitnessAddress("bcrt", 0, Crypto.sha256(Script.write(script)))

bitcoinClient.fundPsbt(Map(address -> 10000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq(address -> 10000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
val FundPsbtResponse(psbt, _, _) = sender.expectMsgType[FundPsbtResponse]

bitcoinClient.processPsbt(psbt).pipeTo(sender.ref)
Expand All @@ -73,22 +73,22 @@ class ExtendedBitcoinClientSpec extends TestKitBaseClass with BitcoindService wi

{
// check that it does work
bitcoinClient.fundPsbt(Map(address -> 10000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq(address -> 10000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
sender.expectMsgType[FundPsbtResponse]
}
{
// invalid address
bitcoinClient.fundPsbt(Map("invalid address" -> 10000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq("invalid address" -> 10000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
sender.expectMsgType[akka.actor.Status.Failure]
}
{
// amount is too small
bitcoinClient.fundPsbt(Map(address -> 100.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq(address -> 100.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
sender.expectMsgType[akka.actor.Status.Failure]
}
{
// amount is too large
bitcoinClient.fundPsbt(Map(address -> 11_000_000.btc), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq(address -> 11_000_000.btc), 0, FundPsbtOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref)
sender.expectMsgType[akka.actor.Status.Failure]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class PsbtSpec extends TestKitBaseClass with BitcoindService with AnyFunSuiteLik

def createOurTx(pub: PublicKey) : (Transaction, Int) = {
val sender = TestProbe()
bitcoinClient.fundPsbt(Map(computeP2WpkhAddress(pub, Block.RegtestGenesisBlock.hash) -> 100000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw, lockUtxos = false)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq(computeP2WpkhAddress(pub, Block.RegtestGenesisBlock.hash) -> 100000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw, lockUtxos = false)).pipeTo(sender.ref)
val FundPsbtResponse(psbt, _, Some(changepos)) = sender.expectMsgType[FundPsbtResponse]
bitcoinClient.processPsbt(psbt, sign = true).pipeTo(sender.ref)
val ProcessPsbtResponse(psbt1, true) = sender.expectMsgType[ProcessPsbtResponse]
Expand All @@ -51,7 +51,7 @@ class PsbtSpec extends TestKitBaseClass with BitcoindService with AnyFunSuiteLik
val (ourTx, index) = createOurTx(priv.publicKey)

// fund a psbt without inputs
bitcoinClient.fundPsbt(Map(computeP2WpkhAddress(priv.publicKey, Block.RegtestGenesisBlock.hash) -> 100000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw, lockUtxos = false)).pipeTo(sender.ref)
bitcoinClient.fundPsbt(Seq(computeP2WpkhAddress(priv.publicKey, Block.RegtestGenesisBlock.hash) -> 100000.sat), 0, FundPsbtOptions(TestConstants.feeratePerKw, lockUtxos = false)).pipeTo(sender.ref)
val FundPsbtResponse(psbt, _, _) = sender.expectMsgType[FundPsbtResponse]

// add our non-wallet input to the PSBT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind
// create a chain of transactions that we don't broadcast yet
val priv = dumpPrivateKey(getNewAddress(probe), probe)
val tx1 = {
bitcoinClient.fundPsbt(Map(Bech32.encodeWitnessAddress("bcrt", 0, priv.publicKey.hash160) -> 150000.sat), locktime = 0, FundPsbtOptions(FeeratePerKw(250 sat), lockUtxos = true)).pipeTo(probe.ref)
bitcoinClient.fundPsbt(Seq(Bech32.encodeWitnessAddress("bcrt", 0, priv.publicKey.hash160) -> 150000.sat), locktime = 0, FundPsbtOptions(FeeratePerKw(250 sat), lockUtxos = true)).pipeTo(probe.ref)
val funded = probe.expectMsgType[FundPsbtResponse].psbt
bitcoinClient.processPsbt(funded, sign = true, sighashType = SIGHASH_ALL).pipeTo(probe.ref)
probe.expectMsgType[ProcessPsbtResponse].psbt.extract().get
Expand Down

0 comments on commit 6b2acab

Please sign in to comment.