mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-23 22:56:52 +01:00
2024 04 16 bitcoindrpc descriptor (#5530)
* Integrate Descriptor class into 'getdescriptorinfo' RPC * WIP: Invalid checksum that is valid according to bitcoin core * Add descriptor.py in comments * Get deriveaddresses RPC working with descriptors * Parse descriptors in DescriptorsResult to Descriptor data type
This commit is contained in:
parent
6f6a78ab52
commit
e143792fb9
12 changed files with 115 additions and 77 deletions
|
@ -5,6 +5,7 @@ import org.bitcoins.core.number.UInt32
|
|||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.util.SeqWrapper
|
||||
import org.bitcoins.core.wallet.fee.BitcoinFeeUnit
|
||||
|
@ -197,7 +198,7 @@ final case class DeriveAddressesResult(addresses: Vector[BitcoinAddress])
|
|||
}
|
||||
|
||||
final case class GetDescriptorInfoResult(
|
||||
descriptor: String,
|
||||
descriptor: Descriptor,
|
||||
checksum: Option[String],
|
||||
isrange: Boolean,
|
||||
issolvable: Boolean,
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.bitcoins.core.currency.Bitcoins
|
|||
import org.bitcoins.core.hd.BIP32Path
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import org.bitcoins.core.protocol.script.{ScriptPubKey, WitnessVersion}
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.script.ScriptType
|
||||
|
@ -468,11 +469,11 @@ object AddressInfoResultPostV21 {
|
|||
|
||||
case class ListDescriptorsResult(
|
||||
wallet_name: String,
|
||||
descriptors: Vector[descriptorsResult]
|
||||
descriptors: Vector[DescriptorsResult]
|
||||
) extends WalletResult
|
||||
|
||||
case class descriptorsResult(
|
||||
desc: String,
|
||||
case class DescriptorsResult(
|
||||
desc: Descriptor,
|
||||
timestamp: ZonedDateTime,
|
||||
active: Boolean,
|
||||
internal: Option[Boolean],
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
|
|||
import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId}
|
||||
import org.bitcoins.core.protocol.ln.routing.{ChannelRoute, NodeRoute, Route}
|
||||
import org.bitcoins.core.protocol.script._
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import org.bitcoins.core.protocol.tlv.{
|
||||
OracleAnnouncementV0TLV,
|
||||
OracleAttestmentV0TLV
|
||||
|
@ -325,6 +326,14 @@ object JsonReaders {
|
|||
ScriptWitness.fromHexOpt)(json)
|
||||
}
|
||||
|
||||
implicit object DescriptorReads extends Reads[Descriptor] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[Descriptor] = {
|
||||
SerializerUtil.processJsStringOpt[Descriptor](Descriptor.fromStringOpt)(
|
||||
json)
|
||||
}
|
||||
}
|
||||
|
||||
implicit object BlockReads extends Reads[Block] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[Block] =
|
||||
|
|
|
@ -521,8 +521,8 @@ object JsonSerializers {
|
|||
)
|
||||
}
|
||||
|
||||
implicit val descriptorsResultReads: Reads[descriptorsResult] =
|
||||
Json.reads[descriptorsResult]
|
||||
implicit val descriptorsResultReads: Reads[DescriptorsResult] =
|
||||
Json.reads[DescriptorsResult]
|
||||
|
||||
implicit val listDescriptorsReads: Reads[ListDescriptorsResult] =
|
||||
Json.reads[ListDescriptorsResult]
|
||||
|
@ -687,9 +687,6 @@ object JsonSerializers {
|
|||
implicit val listWalletsDirResultReads: Reads[ListWalletDirResult] =
|
||||
Json.reads[ListWalletDirResult]
|
||||
|
||||
implicit val deriveAddressesResultReads: Reads[DeriveAddressesResult] =
|
||||
Json.reads[DeriveAddressesResult]
|
||||
|
||||
implicit val submitHeaderResultReads: Reads[SubmitHeaderResult] =
|
||||
Json.reads[SubmitHeaderResult]
|
||||
|
||||
|
@ -915,4 +912,22 @@ object JsonSerializers {
|
|||
implicit val outputMapWrites: Writes[Map[BitcoinAddress, Bitcoins]] =
|
||||
mapWrites[BitcoinAddress, Bitcoins](_.value)
|
||||
|
||||
implicit object DeriveAddressResults extends Reads[DeriveAddressesResult] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[DeriveAddressesResult] = {
|
||||
json match {
|
||||
case str: JsString =>
|
||||
bitcoinAddressReads
|
||||
.reads(str)
|
||||
.map(addr => DeriveAddressesResult(Vector(addr)))
|
||||
case arr: JsArray =>
|
||||
val addresses: Vector[BitcoinAddress] =
|
||||
arr.as[Vector[BitcoinAddress]]
|
||||
JsSuccess(DeriveAddressesResult(addresses))
|
||||
case x =>
|
||||
JsError(s"Invalid response for deriveaddresses rpc, got=$x")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.bitcoins.core.protocol.BitcoinAddress
|
|||
import org.bitcoins.core.protocol.ln.LnInvoice
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.core.protocol.script._
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.psbt._
|
||||
import org.bitcoins.core.script.ScriptType
|
||||
|
@ -110,6 +111,13 @@ object JsonWriters {
|
|||
ScriptPubKeyWrites.writes(o)
|
||||
}
|
||||
|
||||
implicit object DescriptorWrites extends Writes[Descriptor] {
|
||||
|
||||
override def writes(d: Descriptor): JsValue = {
|
||||
JsString(d.toString)
|
||||
}
|
||||
}
|
||||
|
||||
implicit object TransactionInputWrites extends Writes[TransactionInput] {
|
||||
|
||||
override def writes(o: TransactionInput): JsValue =
|
||||
|
|
|
@ -20,8 +20,6 @@ import org.bitcoins.testkit.rpc.{
|
|||
BitcoindRpcTestUtil
|
||||
}
|
||||
import org.scalatest.Assertion
|
||||
|
||||
import java.time.ZonedDateTime
|
||||
import scala.concurrent.Future
|
||||
|
||||
class BitcoindV22RpcClientTest extends BitcoindFixturesCachedPairV22 {
|
||||
|
@ -192,45 +190,6 @@ class BitcoindV22RpcClientTest extends BitcoindFixturesCachedPairV22 {
|
|||
}
|
||||
}
|
||||
|
||||
it should "output wallet name from listdescriptors" in {
|
||||
nodePair: FixtureParam =>
|
||||
val client = nodePair.node1
|
||||
for {
|
||||
_ <- client.unloadWallet("")
|
||||
_ <- client.createWallet("descriptorWalletThree", descriptors = true)
|
||||
resultWallets <- client.listDescriptors(walletName =
|
||||
"descriptorWalletThree")
|
||||
_ <- client.unloadWallet("descriptorWalletThree")
|
||||
_ <- client.loadWallet("")
|
||||
} yield {
|
||||
assert(resultWallets.wallet_name == "descriptorWalletThree")
|
||||
}
|
||||
}
|
||||
|
||||
it should "output descriptors from listdescriptors" in {
|
||||
nodePair: FixtureParam =>
|
||||
val client = nodePair.node1
|
||||
for {
|
||||
_ <- client.unloadWallet("")
|
||||
_ <- client.createWallet("descriptorWalletTwo", descriptors = true)
|
||||
resultWallet <- client.listDescriptors(walletName =
|
||||
"descriptorWalletTwo")
|
||||
_ <- client.unloadWallet("descriptorWalletTwo")
|
||||
_ <- client.loadWallet("")
|
||||
} yield {
|
||||
resultWallet.descriptors.map { d =>
|
||||
assert(
|
||||
d.desc.isInstanceOf[String] && d.timestamp
|
||||
.isInstanceOf[ZonedDateTime]
|
||||
&& d.active.isInstanceOf[Boolean] && d.internal
|
||||
.isInstanceOf[Option[Boolean]]
|
||||
&& d.range.isInstanceOf[Option[Vector[Int]]] && d.next
|
||||
.isInstanceOf[Option[Int]])
|
||||
}
|
||||
succeed
|
||||
}
|
||||
}
|
||||
|
||||
it should "be able to decode a reedem script" in { nodePair: FixtureParam =>
|
||||
val client = nodePair.node1
|
||||
val ecPrivKey1 = ECPrivateKey.freshPrivateKey
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package org.bitcoins.rpc.v23
|
||||
|
||||
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.AddressType
|
||||
import org.bitcoins.commons.jsonmodels.bitcoind.{
|
||||
AddressInfoResultPostV18,
|
||||
AddressInfoResultPostV21,
|
||||
AddressInfoResultPreV18
|
||||
}
|
||||
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.AddressType
|
||||
import org.bitcoins.core.api.chain.db.BlockHeaderDbHelper
|
||||
import org.bitcoins.core.protocol.Bech32mAddress
|
||||
import org.bitcoins.core.protocol.blockchain.RegTestNetChainParams
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import org.bitcoins.rpc.client.common.BitcoindVersion
|
||||
import org.bitcoins.rpc.client.v23.BitcoindV23RpcClient
|
||||
import org.bitcoins.testkit.chain.BlockHeaderHelper
|
||||
|
@ -79,11 +80,12 @@ class BitcoindV23RpcClientTest extends BitcoindFixturesFundedCachedV23 {
|
|||
|
||||
it should "analyze a descriptor" in { client =>
|
||||
val descriptor =
|
||||
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
|
||||
|
||||
Descriptor.fromString(
|
||||
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#gn28ywm7")
|
||||
val descriptorF = client.getDescriptorInfo(descriptor)
|
||||
|
||||
descriptorF.map { result =>
|
||||
assert(result.descriptor == descriptor)
|
||||
assert(result.isrange.==(false))
|
||||
assert(result.issolvable.==(true))
|
||||
assert(result.hasprivatekeys.==(false))
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.bitcoins.core.api.chain.db.BlockHeaderDbHelper
|
|||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.protocol.{Bech32mAddress, BitcoinAddress}
|
||||
import org.bitcoins.core.protocol.blockchain.RegTestNetChainParams
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import org.bitcoins.rpc.client.common.BitcoindVersion
|
||||
import org.bitcoins.rpc.client.v24.BitcoindV24RpcClient
|
||||
import org.bitcoins.testkit.chain.BlockHeaderHelper
|
||||
|
@ -74,11 +75,13 @@ class BitcoindV24RpcClientTest extends BitcoindFixturesFundedCachedV24 {
|
|||
|
||||
it should "analyze a descriptor" in { client =>
|
||||
val descriptor =
|
||||
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
|
||||
Descriptor.fromString(
|
||||
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#gn28ywm7")
|
||||
|
||||
val descriptorF = client.getDescriptorInfo(descriptor)
|
||||
|
||||
descriptorF.map { result =>
|
||||
assert(result.descriptor == descriptor)
|
||||
assert(result.isrange.==(false))
|
||||
assert(result.issolvable.==(true))
|
||||
assert(result.hasprivatekeys.==(false))
|
||||
|
@ -116,4 +119,39 @@ class BitcoindV24RpcClientTest extends BitcoindFixturesFundedCachedV24 {
|
|||
spending <- client.getTxSpendingPrevOut(tx.inputs.head.previousOutput)
|
||||
} yield assert(spending.spendingtxid.contains(txid))
|
||||
}
|
||||
|
||||
it should "derive addresses from a descriptor" in { client =>
|
||||
val str0 =
|
||||
"wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)#t6wfjs64"
|
||||
val descriptor0 = Descriptor.fromString(str0)
|
||||
assert(descriptor0.toString == str0)
|
||||
val addresses0F =
|
||||
client.deriveAddresses(descriptor0, None).map(_.addresses)
|
||||
val expected0 =
|
||||
Vector("bcrt1qjqmxmkpmxt80xz4y3746zgt0q3u3ferr34acd5").map(
|
||||
BitcoinAddress.fromString)
|
||||
val assert0 = addresses0F.map { addresses =>
|
||||
assert(addresses == expected0)
|
||||
}
|
||||
|
||||
val str1 =
|
||||
"wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)#kft60nuy"
|
||||
|
||||
val descriptor1 = Descriptor.fromString(str1)
|
||||
assert(descriptor1.toString == str1)
|
||||
val addresses1F =
|
||||
client.deriveAddresses(descriptor1, Some(Vector(0, 2))).map(_.addresses)
|
||||
val expected1 =
|
||||
Vector("bcrt1qjqmxmkpmxt80xz4y3746zgt0q3u3ferr34acd5",
|
||||
"bcrt1qhku5rq7jz8ulufe2y6fkcpnlvpsta7rq4442dy",
|
||||
"bcrt1qpgptk2gvshyl0s9lqshsmx932l9ccsv265tvaq")
|
||||
.map(BitcoinAddress.fromString)
|
||||
|
||||
val assert1 = assert0.flatMap(_ =>
|
||||
addresses1F.map { addresses =>
|
||||
assert(addresses == expected1)
|
||||
})
|
||||
|
||||
assert1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import org.bitcoins.commons.jsonmodels.bitcoind.{
|
|||
GetDescriptorInfoResult
|
||||
}
|
||||
import org.bitcoins.commons.serializers.JsonSerializers._
|
||||
import play.api.libs.json.{JsString, Json}
|
||||
import org.bitcoins.commons.serializers.JsonWriters.DescriptorWrites
|
||||
import org.bitcoins.core.protocol.script.descriptor.Descriptor
|
||||
import play.api.libs.json.Json
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
|
@ -17,16 +19,19 @@ trait DescriptorRpc {
|
|||
self: Client =>
|
||||
|
||||
def deriveAddresses(
|
||||
descriptor: String,
|
||||
descriptor: Descriptor,
|
||||
range: Option[Vector[Double]]): Future[DeriveAddressesResult] = {
|
||||
val params =
|
||||
if (range.isDefined) List(JsString(descriptor), Json.toJson(range))
|
||||
else List(JsString(descriptor))
|
||||
if (range.isDefined)
|
||||
List(DescriptorWrites.writes(descriptor), Json.toJson(range))
|
||||
else List(DescriptorWrites.writes(descriptor))
|
||||
bitcoindCall[DeriveAddressesResult]("deriveaddresses", params)
|
||||
}
|
||||
|
||||
def getDescriptorInfo(descriptor: String): Future[GetDescriptorInfoResult] = {
|
||||
bitcoindCall[GetDescriptorInfoResult]("getdescriptorinfo",
|
||||
List(JsString(descriptor)))
|
||||
def getDescriptorInfo(
|
||||
descriptor: Descriptor): Future[GetDescriptorInfoResult] = {
|
||||
bitcoindCall[GetDescriptorInfoResult](
|
||||
"getdescriptorinfo",
|
||||
List(DescriptorWrites.writes(descriptor)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
|
|||
|
||||
behavior of "DescriptorChecksumTest"
|
||||
|
||||
val descriptor =
|
||||
val descriptor0 =
|
||||
RawDescriptor(
|
||||
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deadbeef")),
|
||||
None)
|
||||
|
@ -17,26 +17,26 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
|
|||
val (payload, checksum) = (split0(0), split0(1))
|
||||
assert(Descriptor.createChecksum(payload) == checksum)
|
||||
|
||||
assert(Descriptor.isValidChecksum(descriptor, Some(checksum)))
|
||||
assert(Descriptor.isValidChecksum(descriptor0, Some(checksum)))
|
||||
|
||||
//expression with nochecksum should be valid
|
||||
assert(Descriptor.isValidChecksum(descriptor, None))
|
||||
assert(Descriptor.isValidChecksum(descriptor0, None))
|
||||
|
||||
// val descriptor1 =
|
||||
// Descriptor.fromString(
|
||||
// "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)")
|
||||
// val checksum1 = "cjjspncu"
|
||||
// assert(Descriptor.createChecksum(descriptor1) == checksum1)
|
||||
// assert(Descriptor.isValidChecksum(descriptor1, Some(checksum1)))
|
||||
val descriptor1 =
|
||||
Descriptor.fromString(
|
||||
"wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)")
|
||||
val checksum1 = "cjjspncu"
|
||||
assert(Descriptor.isValidChecksum(descriptor1, Some(checksum1)))
|
||||
assert(Descriptor.createChecksum(descriptor1) == checksum1)
|
||||
}
|
||||
|
||||
it must "fail when a bad checksum is given" in {
|
||||
//Missing checksum
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("#")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor0, Some("#")))
|
||||
//Too long checksum (9 chars)
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spxmx")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor0, Some("89f8spxmx")))
|
||||
//Too short checksum (7 chars)
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spx")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor0, Some("89f8spx")))
|
||||
//Error in payload
|
||||
val bad =
|
||||
RawDescriptor(
|
||||
|
@ -44,6 +44,6 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
|
|||
None)
|
||||
assert(!Descriptor.isValidChecksum(bad, Some("89f8spxm")))
|
||||
//Error in checksum
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("#9f8spxm")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor0, Some("#9f8spxm")))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ object ExtKey extends Factory[ExtKey] with StringFactory[ExtKey] {
|
|||
|
||||
val masterFingerprint: ByteVector = hex"00000000"
|
||||
|
||||
val prefixes: Vector[String] = Vector("xprv", "xpub")
|
||||
val prefixes: Vector[String] = Vector("xprv", "xpub", "tprv", "tpub")
|
||||
|
||||
/** Takes in a base58 string and tries to convert it to an extended key */
|
||||
override def fromString(base58: String): ExtKey = {
|
||||
|
|
|
@ -142,8 +142,8 @@ sealed abstract class DescriptorFactory[
|
|||
val checksumOpt =
|
||||
if (checksum.nonEmpty) Some(checksum.tail) else None //drop '#'
|
||||
val isValidChecksum = Descriptor.isValidChecksum(
|
||||
descriptor = createDescriptor(expression, None),
|
||||
checksumOpt = checksumOpt)
|
||||
createDescriptor(expression, None),
|
||||
checksumOpt)
|
||||
if (isValidChecksum) {
|
||||
createDescriptor(expression, checksumOpt)
|
||||
} else {
|
||||
|
|
Loading…
Add table
Reference in a new issue