mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-03 10:46:42 +01:00
Let wallet sign PSBTs (#2236)
* Let wallet sign PSBTs * Add example to docs * Add logs and test case
This commit is contained in:
parent
dfe6b70781
commit
685d5b0273
12 changed files with 344 additions and 63 deletions
|
@ -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])
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -486,6 +486,7 @@ case class PSBT(
|
|||
MapType <: PSBTMap[RecordType]](
|
||||
extKey: ExtKey,
|
||||
path: BIP32Path,
|
||||
pubKey: ECPublicKey,
|
||||
index: Int,
|
||||
keyIdByte: Byte,
|
||||
maps: Vector[MapType],
|
||||
|
@ -497,30 +498,23 @@ case class PSBT(
|
|||
require(!isFinalized, "Cannot update a PSBT that is finalized")
|
||||
|
||||
val previousElements = maps(index).elements
|
||||
val keyT = extKey.deriveChildPubKey(path)
|
||||
lazy val expectedBytes = pubKey.bytes.+:(keyIdByte)
|
||||
|
||||
keyT match {
|
||||
case Success(key) =>
|
||||
lazy val expectedBytes = key.bytes.+:(keyIdByte)
|
||||
|
||||
val elements =
|
||||
if (!previousElements.exists(_.key == expectedBytes)) {
|
||||
val fp =
|
||||
if (extKey.fingerprint == ExtKey.masterFingerprint) {
|
||||
extKey.deriveChildPubKey(path.head).get.fingerprint
|
||||
} else {
|
||||
extKey.fingerprint
|
||||
}
|
||||
|
||||
previousElements :+ makeRecord(key.key, fp, path)
|
||||
val elements =
|
||||
if (!previousElements.exists(_.key == expectedBytes)) {
|
||||
val fp =
|
||||
if (extKey.fingerprint == ExtKey.masterFingerprint) {
|
||||
extKey.deriveChildPubKey(path.head).get.fingerprint
|
||||
} else {
|
||||
previousElements
|
||||
extKey.fingerprint
|
||||
}
|
||||
|
||||
maps.updated(index, makeMap(elements))
|
||||
case Failure(err) =>
|
||||
throw err
|
||||
}
|
||||
previousElements :+ makeRecord(pubKey, fp, path)
|
||||
} else {
|
||||
previousElements
|
||||
}
|
||||
|
||||
maps.updated(index, makeMap(elements))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue