Let wallet sign PSBTs (#2236)

* Let wallet sign PSBTs

* Add example to docs

* Add logs and test case
This commit is contained in:
Ben Carman 2020-11-07 09:25:59 -06:00 committed by GitHub
parent dfe6b70781
commit 685d5b0273
12 changed files with 344 additions and 63 deletions

View file

@ -823,6 +823,20 @@ object ConsoleCli {
case other => other
}))
),
cmd("signpsbt")
.action((_, conf) => conf.copy(command = SignPSBT(PSBT.empty)))
.text("Signs the PSBT's inputs with keys that are associated with the wallet")
.children(
arg[PSBT]("psbt")
.text("PSBT to sign")
.required()
.action((psbt, conf) =>
conf.copy(command = conf.command match {
case signPSBT: SignPSBT =>
signPSBT.copy(psbt = psbt)
case other => other
}))
),
cmd("opreturncommit")
.action((_, conf) =>
conf.copy(command = OpReturnCommit("", hashMessage = false, None)))
@ -1259,6 +1273,8 @@ object ConsoleCli {
Seq(up.writeJs(message),
up.writeJs(hashMessage),
up.writeJs(satoshisPerVirtualByte)))
case SignPSBT(psbt) =>
RequestParam("signpsbt", Seq(up.writeJs(psbt)))
// height
case GetBlockCount => RequestParam("getblockcount")
// filter count
@ -1518,6 +1534,8 @@ object CliCommand {
feeRateOpt: Option[SatoshisPerVirtualByte])
extends CliCommand
case class SignPSBT(psbt: PSBT) extends CliCommand
case class LockUnspent(
unlock: Boolean,
outPoints: Vector[LockUnspentOutputParameter])

View file

@ -958,6 +958,28 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
}
}
"sign a psbt" in {
val tx = Transaction(
"020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000")
(mockWalletApi
.signPSBT(_: PSBT)(_: ExecutionContext))
.expects(PSBT.empty, executor)
.returning(Future.successful(PSBT.fromUnsignedTx(tx)))
.anyNumberOfTimes()
val route =
walletRoutes.handleCommand(
ServerCommand("signpsbt", Arr(Str(PSBT.empty.hex))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[String] == s"""{"result":"${PSBT
.fromUnsignedTx(tx)
.base64}","error":null}""")
}
}
"make an OP_RETURN commitment" in {
val message = "Never gonna give you up, never gonna let you down"

View file

@ -476,6 +476,18 @@ object SendWithAlgo extends ServerJsonModels {
}
case class SignPSBT(psbt: PSBT)
object SignPSBT extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[SignPSBT] = {
require(jsArr.arr.size == 1,
s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1")
Try(SignPSBT(jsToPSBT(jsArr.arr.head)))
}
}
case class OpReturnCommit(
message: String,
hashMessage: Boolean,

View file

@ -266,6 +266,18 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit system: ActorSystem)
}
}
case ServerCommand("signpsbt", arr) =>
SignPSBT.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(SignPSBT(psbt)) =>
complete {
wallet.signPSBT(psbt).map { signed =>
Server.httpSuccess(signed.base64)
}
}
}
case ServerCommand("opreturncommit", arr) =>
OpReturnCommit.fromJsArr(arr) match {
case Failure(exception) =>

View file

@ -3,7 +3,7 @@ package org.bitcoins.core.psbt
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{CurrencyUnits, Satoshis}
import org.bitcoins.core.hd.BIP32Path
import org.bitcoins.core.number.{Int32, UInt32}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt.GlobalPSBTRecord.{UnsignedTransaction, Version}
@ -15,13 +15,9 @@ import org.bitcoins.core.psbt.PSBTGlobalKeyId.XPubKeyKeyId
import org.bitcoins.core.script.constant._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.utxo.{ConditionalPath, InputInfo}
import org.bitcoins.crypto.{
DoubleSha256Digest,
ECPublicKey,
Sha256Hash160Digest,
Sign
}
import org.bitcoins.crypto.{ECPublicKey, Sha256Hash160Digest, Sign}
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.bitcoins.testkit.util.TransactionTestUtil._
import scodec.bits._
class PSBTUnitTest extends BitcoinSAsyncTest {
@ -74,6 +70,19 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
.fromString(
"tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF")
val bip32Paths = Vector(
BIP32Path.fromString("m/0'/0'/0'"),
BIP32Path.fromString("m/0'/0'/1'"),
BIP32Path.fromString("m/0'/0'/2'"),
BIP32Path.fromString("m/0'/0'/3'"),
BIP32Path.fromString("m/0'/0'/4'"),
BIP32Path.fromString("m/0'/0'/5'")
)
val keys = bip32Paths.map { path =>
extKey.deriveChildPubKey(path).get.key
}
val psbt = start
.addUTXOToInput(
Transaction(
@ -99,12 +108,12 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
hex"522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae"),
1
)
.addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/0'"), 0)
.addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/1'"), 0)
.addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/2'"), 1)
.addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/3'"), 1)
.addKeyPathToOutput(extKey, BIP32Path.fromString("m/0'/0'/4'"), 0)
.addKeyPathToOutput(extKey, BIP32Path.fromString("m/0'/0'/5'"), 1)
.addKeyPathToInput(extKey, bip32Paths(0), keys(0), 0)
.addKeyPathToInput(extKey, bip32Paths(1), keys(1), 0)
.addKeyPathToInput(extKey, bip32Paths(2), keys(2), 1)
.addKeyPathToInput(extKey, bip32Paths(3), keys(3), 1)
.addKeyPathToOutput(extKey, bip32Paths(4), keys(4), 0)
.addKeyPathToOutput(extKey, bip32Paths(5), keys(5), 1)
assert(psbt == expected)
@ -347,29 +356,6 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
}
}
def dummyTx(
prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty,
scriptSig: ScriptSignature = EmptyScriptSignature,
spk: ScriptPubKey = EmptyScriptPubKey): Transaction = {
BaseTransaction(
version = Int32.zero,
inputs = Vector(
TransactionInput(outPoint = TransactionOutPoint(txId = prevTxId,
vout = UInt32.zero),
scriptSignature = scriptSig,
sequenceNumber = UInt32.zero)),
outputs = Vector(TransactionOutput(CurrencyUnits.oneBTC, spk)),
lockTime = UInt32.zero
)
}
def dummyPSBT(
prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty,
scriptSig: ScriptSignature = EmptyScriptSignature,
spk: ScriptPubKey = EmptyScriptPubKey): PSBT = {
PSBT.fromUnsignedTx(dummyTx(prevTxId, scriptSig, spk))
}
it must "successfully change a NonWitnessUTXO to a WitnessUTXO when compressing" in {
// Create non BIP-143 vulnerable witness utxo
val dummyData = ECPublicKey.freshPublicKey.bytes

View file

@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.transaction.{
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.keymanagement.KeyManagerParams
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
@ -406,6 +407,8 @@ trait HDWalletApi extends WalletApi {
} yield tx
}
def signPSBT(psbt: PSBT)(implicit ec: ExecutionContext): Future[PSBT]
def makeOpReturnCommitment(
message: String,
hashMessage: Boolean,

View file

@ -486,6 +486,7 @@ case class PSBT(
MapType <: PSBTMap[RecordType]](
extKey: ExtKey,
path: BIP32Path,
pubKey: ECPublicKey,
index: Int,
keyIdByte: Byte,
maps: Vector[MapType],
@ -497,11 +498,7 @@ case class PSBT(
require(!isFinalized, "Cannot update a PSBT that is finalized")
val previousElements = maps(index).elements
val keyT = extKey.deriveChildPubKey(path)
keyT match {
case Success(key) =>
lazy val expectedBytes = key.bytes.+:(keyIdByte)
lazy val expectedBytes = pubKey.bytes.+:(keyIdByte)
val elements =
if (!previousElements.exists(_.key == expectedBytes)) {
@ -512,15 +509,12 @@ case class PSBT(
extKey.fingerprint
}
previousElements :+ makeRecord(key.key, fp, path)
previousElements :+ makeRecord(pubKey, fp, path)
} else {
previousElements
}
maps.updated(index, makeMap(elements))
case Failure(err) =>
throw err
}
}
/**
@ -530,10 +524,15 @@ case class PSBT(
* @param index index of the InputPSBTMap to add the BIP32Path to
* @return PSBT with added BIP32Path
*/
def addKeyPathToInput(extKey: ExtKey, path: BIP32Path, index: Int): PSBT = {
def addKeyPathToInput(
extKey: ExtKey,
path: BIP32Path,
pubKey: ECPublicKey,
index: Int): PSBT = {
val newInputMaps = addKeyPathToMap[InputPSBTRecord, InputPSBTMap](
extKey = extKey,
path = path,
pubKey = pubKey,
index = index,
keyIdByte = PSBTInputKeyId.BIP32DerivationPathKeyId.byte,
maps = inputMaps,
@ -551,10 +550,15 @@ case class PSBT(
* @param index index of the OutputPSBTMap to add the BIP32Path to
* @return PSBT with added BIP32Path
*/
def addKeyPathToOutput(extKey: ExtKey, path: BIP32Path, index: Int): PSBT = {
def addKeyPathToOutput(
extKey: ExtKey,
path: BIP32Path,
pubKey: ECPublicKey,
index: Int): PSBT = {
val newOutputMaps = addKeyPathToMap[OutputPSBTRecord, OutputPSBTMap](
extKey = extKey,
path = path,
pubKey = pubKey,
index = index,
keyIdByte = PSBTOutputKeyId.BIP32DerivationPathKeyId.byte,
maps = outputMaps,

View file

@ -178,6 +178,8 @@ For more information on how to use our built in `cli` to interact with the serve
- `amount` - Amount to send in BTC
- `algo` - Coin selection algo
- `--feerate <value>` - Fee rate in sats per virtual byte
- `signpsbt` `psbt` - Signs the PSBT's inputs with keys that are associated with the wallet
- `psbt` - PSBT to sign
- `opreturncommit` `message` `[options]` - Creates OP_RETURN commitment transaction
- `message` - message to put into OP_RETURN commitment
- `--hashMessage` - should the message be hashed before commitment
@ -206,3 +208,19 @@ For more information on how to use our built in `cli` to interact with the serve
- `psbt` - PSBT serialized in hex or base64 format
- `converttopsbt` `unsignedTx` - Creates an empty psbt from the given transaction
- `unsignedTx` - serialized unsigned transaction in hex
### Sign PSBT with Wallet Example
Bitcoin-S CLI:
```bash
$ bitcoin-s-cli signpsbt cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA
cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA
```
CURL:
```bash
$ curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "signpsbt", "params": ["cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA"]}' -H "Content-Type: application/json" http://127.0.0.1:9999/
{"result":"cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA","error":null}
```

View file

@ -5,8 +5,9 @@ import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.number.{Int32, UInt32}
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.crypto.ECPublicKey
import org.bitcoins.crypto.{DoubleSha256Digest, ECPublicKey}
/**
* Created by chris on 2/12/16.
@ -249,6 +250,29 @@ trait TransactionTestUtil extends BitcoinSLogger {
outputs = Vector(output),
lockTime = TransactionConstants.lockTime)
}
def dummyTx(
prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty,
scriptSig: ScriptSignature = EmptyScriptSignature,
spk: ScriptPubKey = EmptyScriptPubKey): Transaction = {
BaseTransaction(
version = Int32.zero,
inputs = Vector(
TransactionInput(outPoint = TransactionOutPoint(txId = prevTxId,
vout = UInt32.zero),
scriptSignature = scriptSig,
sequenceNumber = UInt32.zero)),
outputs = Vector(TransactionOutput(CurrencyUnits.oneBTC, spk)),
lockTime = UInt32.zero
)
}
def dummyPSBT(
prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty,
scriptSig: ScriptSignature = EmptyScriptSignature,
spk: ScriptPubKey = EmptyScriptPubKey): PSBT = {
PSBT.fromUnsignedTx(dummyTx(prevTxId, scriptSig, spk))
}
}
object TransactionTestUtil extends TransactionTestUtil

View file

@ -3,15 +3,22 @@ package org.bitcoins.wallet
import java.nio.file.Files
import org.bitcoins.core.api.wallet.NeutrinoWalletApi.BlockMatchingResponse
import org.bitcoins.core.api.wallet.db.AddressDb
import org.bitcoins.core.api.wallet.db.{AddressDb, TransactionDbHelper}
import org.bitcoins.core.hd.HDChainType.{Change, External}
import org.bitcoins.core.hd.{HDAccount, HDChainType}
import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.{
MultiSignatureScriptPubKey,
P2PKHScriptPubKey,
P2SHScriptPubKey,
P2WPKHWitnessSPKV0
}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.keymanagement.KeyManagerUnlockError
import org.bitcoins.core.wallet.keymanagement.KeyManagerUnlockError.MnemonicNotFound
import org.bitcoins.crypto.AesPassword
import org.bitcoins.crypto.{AesPassword, ECPublicKey}
import org.bitcoins.keymanager.WalletStorage
import org.bitcoins.testkit.util.TransactionTestUtil._
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import org.scalatest.compatible.Assertion
@ -196,4 +203,104 @@ class WalletUnitTest extends BitcoinSWalletTest {
}
}
}
it must "be able to sign a psbt with a key path" in { wallet: Wallet =>
val dummyKey = ECPublicKey.freshPublicKey
for {
accountDb <- wallet.accountDAO.findAll().map(_.head)
addr <- wallet.getNewAddress(accountDb)
addrDb <- wallet.addressDAO.findAddress(addr).map(_.get)
walletKey = addrDb.ecPublicKey
walletPath = addrDb.path
spk = MultiSignatureScriptPubKey(2, Vector(dummyKey, walletKey))
dummyPrevTx = dummyTx(spk = spk)
prevTxDb = TransactionDbHelper.fromTransaction(dummyPrevTx)
_ <- wallet.transactionDAO.create(prevTxDb)
psbt = dummyPSBT(prevTxId = dummyPrevTx.txId)
.addKeyPathToInput(accountDb.xpub, walletPath, walletKey, 0)
signed <- wallet.signPSBT(psbt)
} yield {
assert(signed != psbt)
assert(
signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey))
}
}
it must "be able to sign a psbt with our own p2pkh utxo" in {
wallet: Wallet =>
for {
addr <- wallet.getNewAddress(AddressType.Legacy)
addrDb <- wallet.addressDAO.findAddress(addr).map(_.get)
walletKey = addrDb.ecPublicKey
spk = addr.scriptPubKey
_ = assert(spk == P2PKHScriptPubKey(walletKey))
dummyPrevTx = dummyTx(spk = spk)
_ <- wallet.processTransaction(dummyPrevTx, blockHashOpt = None)
psbt = dummyPSBT(prevTxId = dummyPrevTx.txId)
signed <- wallet.signPSBT(psbt)
} yield {
assert(signed != psbt)
assert(
signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey))
}
}
it must "be able to sign a psbt with our own p2sh segwit utxo" in {
wallet: Wallet =>
for {
addr <- wallet.getNewAddress(AddressType.NestedSegWit)
addrDb <- wallet.addressDAO.findAddress(addr).map(_.get)
walletKey = addrDb.ecPublicKey
spk = addr.scriptPubKey
_ = assert(spk == P2SHScriptPubKey(P2WPKHWitnessSPKV0(walletKey)))
dummyPrevTx = dummyTx(spk = spk)
_ <- wallet.processTransaction(dummyPrevTx, blockHashOpt = None)
psbt = dummyPSBT(prevTxId = dummyPrevTx.txId)
signed <- wallet.signPSBT(psbt)
} yield {
assert(signed != psbt)
assert(
signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey))
}
}
it must "be able to sign a psbt with our own p2wpkh utxo" in {
wallet: Wallet =>
for {
addr <- wallet.getNewAddress(AddressType.SegWit)
addrDb <- wallet.addressDAO.findAddress(addr).map(_.get)
walletKey = addrDb.ecPublicKey
spk = addr.scriptPubKey
_ = assert(spk == P2WPKHWitnessSPKV0(walletKey))
dummyPrevTx = dummyTx(spk = spk)
_ <- wallet.processTransaction(dummyPrevTx, blockHashOpt = None)
psbt = dummyPSBT(prevTxId = dummyPrevTx.txId)
.addUTXOToInput(dummyPrevTx, 0)
signed <- wallet.signPSBT(psbt)
} yield {
assert(signed != psbt)
assert(
signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey))
}
}
it must "be able to sign a psbt with no wallet utxos" in { wallet: Wallet =>
val psbt = dummyPSBT()
for {
signed <- wallet.signPSBT(psbt)
} yield assert(signed == psbt)
}
}

View file

@ -13,11 +13,12 @@ import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.currency._
import org.bitcoins.core.gcs.{GolombFilter, SimpleFilterMatcher}
import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurpose, HDPurposes}
import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.ChainParams
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.script.constant.ScriptConstant
import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.util.{BitcoinScriptUtil, FutureUtil, HDUtil}
@ -612,6 +613,73 @@ abstract class Wallet
} yield tx
}
override def signPSBT(psbt: PSBT)(implicit
ec: ExecutionContext): Future[PSBT] = {
val inputTxIds = psbt.transaction.inputs.zipWithIndex.map {
case (input, index) =>
input.previousOutput.txIdBE -> index
}.toMap
for {
accountDbs <- accountDAO.findAll()
ourXpubs = accountDbs.map(_.xpub)
utxos <- spendingInfoDAO.findAll()
txs <- transactionDAO.findByTxIds(inputTxIds.keys.toVector)
updated = txs.foldLeft(psbt) { (accum, tx) =>
val index = inputTxIds(tx.txIdBE)
accum.addUTXOToInput(tx.transaction, index)
}
signed <-
FutureUtil.foldLeftAsync(updated, updated.inputMaps.zipWithIndex) {
case (unsigned, (input, index)) =>
val xpubKeyPaths = input.BIP32DerivationPaths
.filter { path =>
ourXpubs.exists(_.fingerprint == path.masterFingerprint)
}
.map(bip32Path =>
HDPath.fromString(
bip32Path.path.toString
)) // TODO add a way to get a HDPath from a BIP32 Path
val (utxoPath, withData) = {
val outPoint = unsigned.transaction.inputs(index).previousOutput
utxos.find(_.outpoint == outPoint) match {
case Some(utxo) =>
val psbtWithUtxoData = utxo.redeemScript match {
case Some(redeemScript) =>
unsigned.addRedeemOrWitnessScriptToInput(redeemScript,
index)
case None => unsigned
}
(Vector(utxo.path), psbtWithUtxoData)
case None => (Vector.empty, unsigned)
}
}
val keyPaths = xpubKeyPaths ++ utxoPath
FutureUtil.foldLeftAsync(withData, keyPaths) { (accum, hdPath) =>
val sign = keyManager.toSign(hdPath)
// Only sign if that key doesn't have a signature yet
if (!input.partialSignatures.exists(_.pubKey == sign.publicKey)) {
logger.debug(
s"Signing input $index with key ${sign.publicKey.hex}")
accum.sign(index, sign)
} else {
Future.successful(accum)
}
}
}
} yield {
if (updated == signed) {
logger.warn("Did not find any keys or utxos that belong to this wallet")
}
signed
}
}
protected def getLastAccountOpt(
purpose: HDPurpose): Future[Option[AccountDb]] = {
accountDAO

View file

@ -58,6 +58,13 @@ trait TxDAO[DbEntryType <: TxDB]
findByTxId(outPoint.txId)
}
def findByTxIds(
txIdBEs: Vector[DoubleSha256DigestBE]): Future[Vector[DbEntryType]] = {
val q = table.filter(_.txIdBE.inSet(txIdBEs))
safeDatabase.runVec(q.result.transactionally)
}
def findByTxId(txIdBE: DoubleSha256DigestBE): Future[Option[DbEntryType]] = {
val q = table
.filter(_.txIdBE === txIdBE)