Skip to content

Commit 7e76be6

Browse files
committed
feat: Add derivation path child and extend methods
1 parent af2475e commit 7e76be6

File tree

3 files changed

+197
-4
lines changed

3 files changed

+197
-4
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package org.bitcoindevkit
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertTrue
6+
import kotlin.test.assertFalse
7+
import androidx.test.ext.junit.runners.AndroidJUnit4
8+
import org.junit.runner.RunWith
9+
import kotlin.test.assertFailsWith
10+
11+
@RunWith(AndroidJUnit4::class)
12+
class DerivationPathTest {
13+
@Test
14+
fun derivationPathFromString() {
15+
val pathString = "m/84h/0h/0h/0/0"
16+
val expectedPath = "84'/0'/0'/0/0"
17+
val derivationPath = DerivationPath(pathString)
18+
assertEquals(
19+
expected = expectedPath,
20+
actual = derivationPath.toString()
21+
)
22+
}
23+
24+
@Test
25+
fun derivationPathtoString() {
26+
val pathString = "m/44h/1h/0h/1/5"
27+
val expectedPath = "44'/1'/0'/1/5"
28+
val derivationPath = DerivationPath(pathString)
29+
assertEquals(
30+
expected = expectedPath,
31+
actual = derivationPath.toString()
32+
)
33+
}
34+
35+
@Test
36+
fun correctlyIdentifiesMaster() {
37+
val pathString = "m"
38+
val derivationPath = DerivationPath(pathString)
39+
assertTrue(derivationPath.isMaster())
40+
41+
val nonMasterPathString = "m/0h/1/2"
42+
val nonMasterDerivationPath = DerivationPath(nonMasterPathString)
43+
assertFalse(nonMasterDerivationPath.isMaster())
44+
45+
val masterPathString = DerivationPath.master()
46+
assertTrue(masterPathString.isMaster())
47+
}
48+
49+
@Test
50+
fun invalidDerivationPath(){
51+
val invalidPathString = "invalid/path/string"
52+
assertFailsWith<Bip32Exception.InvalidChildNumberFormat> { DerivationPath(invalidPathString) }
53+
}
54+
55+
@Test
56+
fun createChildDerivationPath() {
57+
val parentPathString = "m/44h/0h"
58+
val childIndex = 5u
59+
val expectedNormalChildPath = "44'/0'/5"
60+
val parentDerivationPath = DerivationPath(parentPathString)
61+
val childDerivationPath = parentDerivationPath.child(ChildNumber.Normal(childIndex))
62+
63+
assertEquals(
64+
expected = expectedNormalChildPath,
65+
actual = childDerivationPath.toString()
66+
)
67+
68+
val expectedHardenedChildPath = "44'/0'/5'"
69+
val hardenedChildDerivationPath = parentDerivationPath.child(ChildNumber.Hardened(childIndex))
70+
71+
assertEquals(
72+
expected = expectedHardenedChildPath,
73+
actual = hardenedChildDerivationPath.toString()
74+
)
75+
}
76+
77+
@Test
78+
fun checkInvalidNormalChildNumberFails(){
79+
val parentPathString = "m/44h/0h"
80+
val aHardenedchildIndex = 2147483648u
81+
val parentDerivationPath = DerivationPath(parentPathString)
82+
83+
assertFailsWith<Bip32Exception.InvalidChildNumber> { parentDerivationPath.child(ChildNumber.Normal(aHardenedchildIndex)) }
84+
}
85+
86+
@Test
87+
fun checkInvalidHardenedChildNumberFails(){
88+
val parentPathString = "m/44h/0h"
89+
val alreadyHardenedIndex = 2147483649u
90+
val parentDerivationPath = DerivationPath(parentPathString)
91+
92+
assertFailsWith<Bip32Exception.InvalidChildNumber> { parentDerivationPath.child(ChildNumber.Hardened(alreadyHardenedIndex)) }
93+
}
94+
95+
@Test
96+
fun extendDerivationPath(){
97+
val basePathString = "m/84h/0h"
98+
val extensions = "0h/1/2"
99+
100+
val expectedExtendedPath = "84'/0'/0'/1/2"
101+
102+
val baseDerivationPath = DerivationPath(basePathString)
103+
val extensionDerivationPath = DerivationPath(extensions)
104+
105+
val extendedDerivationPath = baseDerivationPath.extend(extensionDerivationPath)
106+
assertEquals(
107+
expected = expectedExtendedPath,
108+
actual = extendedDerivationPath.toString()
109+
)
110+
}
111+
112+
@Test
113+
fun conversionToList() {
114+
val pathString = "m/49h/1h/0h/0/10"
115+
val derivationPathFromString = DerivationPath(pathString)
116+
117+
val derivationList = derivationPathFromString.toU32Vec()
118+
119+
// BIP32 standard (0x80000000 or 2147483648) converted to decimal and added to each hardened index:
120+
121+
// 49h = 49 + 2147483648 = 2147483697
122+
// 1h = 1 + 2147483648 = 2147483649
123+
// 0h = 0 + 2147483648 = 2147483648
124+
val expectedListString = "[2147483697, 2147483649, 2147483648, 0, 10]"
125+
126+
assertEquals(
127+
expected = expectedListString,
128+
actual = derivationList.toString()
129+
)
130+
}
131+
}

bdk-ffi/src/bitcoin.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
use crate::error::{
2-
AddressParseError, ExtractTxError, FeeRateError, FromScriptError, HashParseError, PsbtError,
3-
PsbtParseError, TransactionError,
2+
AddressParseError, Bip32Error, ExtractTxError, FeeRateError, FromScriptError, HashParseError,
3+
PsbtError, PsbtParseError, TransactionError,
44
};
55
use crate::error::{ParseAmountError, PsbtFinalizeError};
66
use crate::keys::DerivationPath;
77

88
use crate::{impl_from_core_type, impl_hash_like, impl_into_core_type};
9-
use std::collections::HashMap;
10-
119
use bdk_wallet::bitcoin::address::NetworkChecked;
1210
use bdk_wallet::bitcoin::address::NetworkUnchecked;
1311
use bdk_wallet::bitcoin::address::{Address as BdkAddress, AddressData as BdkAddressData};
@@ -22,7 +20,10 @@ use bdk_wallet::bitcoin::io::Cursor;
2220
use bdk_wallet::bitcoin::psbt::Input as BdkInput;
2321
use bdk_wallet::bitcoin::psbt::Output as BdkOutput;
2422
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
23+
use std::collections::HashMap;
24+
use std::convert::TryFrom;
2525

26+
use bdk_wallet::bitcoin::bip32::ChildNumber as BdkChildNumber;
2627
use bdk_wallet::bitcoin::taproot::LeafNode as BdkLeafNode;
2728
use bdk_wallet::bitcoin::taproot::NodeInfo as BdkNodeInfo;
2829
use bdk_wallet::bitcoin::taproot::TapTree as BdkTapTree;
@@ -1264,6 +1265,45 @@ impl From<TxOut> for BdkTxOut {
12641265
}
12651266
}
12661267

1268+
/// A child number in a derivation path
1269+
#[derive(Copy, Clone, uniffi::Enum)]
1270+
pub enum ChildNumber {
1271+
/// Non-hardened key
1272+
Normal {
1273+
/// Key index, within [0, 2^31 - 1]
1274+
index: u32,
1275+
},
1276+
/// Hardened key
1277+
Hardened {
1278+
/// Key index, within [0, 2^31 - 1]
1279+
index: u32,
1280+
},
1281+
}
1282+
1283+
impl From<BdkChildNumber> for ChildNumber {
1284+
fn from(value: BdkChildNumber) -> Self {
1285+
match value {
1286+
BdkChildNumber::Normal { index } => ChildNumber::Normal { index },
1287+
BdkChildNumber::Hardened { index } => ChildNumber::Hardened { index },
1288+
}
1289+
}
1290+
}
1291+
1292+
impl TryFrom<ChildNumber> for BdkChildNumber {
1293+
type Error = Bip32Error;
1294+
1295+
fn try_from(value: ChildNumber) -> Result<Self, Self::Error> {
1296+
match value {
1297+
ChildNumber::Normal { index } => {
1298+
BdkChildNumber::from_normal_idx(index).map_err(Bip32Error::from)
1299+
}
1300+
ChildNumber::Hardened { index } => {
1301+
BdkChildNumber::from_hardened_idx(index).map_err(Bip32Error::from)
1302+
}
1303+
}
1304+
}
1305+
}
1306+
12671307
/// A bitcoin Block hash
12681308
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, std::hash::Hash, uniffi::Object)]
12691309
#[uniffi::export(Display, Eq, Hash, Ord)]

bdk-ffi/src/keys.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use crate::bitcoin::ChildNumber;
12
use crate::error::{Bip32Error, Bip39Error, DescriptorKeyError};
23
use crate::{impl_from_core_type, impl_into_core_type};
34

5+
use bdk_wallet::bitcoin::bip32::ChildNumber as BdkChildNumber;
46
use bdk_wallet::bitcoin::bip32::DerivationPath as BdkDerivationPath;
57
use bdk_wallet::bitcoin::key::Secp256k1;
68
use bdk_wallet::bitcoin::secp256k1::rand;
@@ -15,6 +17,7 @@ use bdk_wallet::keys::{
1517
use bdk_wallet::miniscript::descriptor::{DescriptorXKey, Wildcard};
1618
use bdk_wallet::miniscript::BareCtx;
1719

20+
use std::convert::TryFrom;
1821
use std::fmt::Display;
1922
use std::str::FromStr;
2023
use std::sync::Arc;
@@ -100,6 +103,25 @@ impl DerivationPath {
100103
pub fn is_empty(&self) -> bool {
101104
self.0.is_empty()
102105
}
106+
107+
/// Create a new DerivationPath that is a child of this one.
108+
pub fn child(&self, child_number: ChildNumber) -> Result<Arc<Self>, Bip32Error> {
109+
let validated_child_number = BdkChildNumber::try_from(child_number)?;
110+
Ok(Arc::new(self.0.child(validated_child_number).into()))
111+
}
112+
113+
/// Concatenate `self` with `path` and return the resulting new path.
114+
pub fn extend(&self, other: &DerivationPath) -> Arc<Self> {
115+
let extended_path = self.0.extend(&other.0);
116+
Arc::new(DerivationPath(extended_path))
117+
}
118+
119+
/// Returns the derivation path as a vector of u32 integers.
120+
/// Unhardened elements are copied as is.
121+
/// 0x80000000 is added to the hardened elements.
122+
pub fn to_u32_vec(&self) -> Vec<u32> {
123+
self.0.to_u32_vec()
124+
}
103125
}
104126

105127
impl Display for DerivationPath {

0 commit comments

Comments
 (0)