Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub struct DerivedKey {
pub p2pkh_compressed: String,
/// P2PKH address (uncompressed pubkey)
pub p2pkh_uncompressed: String,
/// P2SH-P2WPKH (wrapped segwit) address
pub p2sh_p2wpkh: String,
/// P2WPKH (bech32) address
pub p2wpkh: String,
/// P2TR (Taproot bech32m) address
Expand All @@ -44,10 +46,11 @@ pub struct DerivedKey {

impl DerivedKey {
/// Get all addresses as slice for matching.
pub fn addresses(&self) -> [&str; 4] {
pub fn addresses(&self) -> [&str; 5] {
[
&self.p2pkh_compressed,
&self.p2pkh_uncompressed,
&self.p2sh_p2wpkh,
&self.p2wpkh,
&self.p2tr,
]
Expand Down Expand Up @@ -115,9 +118,10 @@ impl KeyDeriver {
let p2pkh_compressed = Address::p2pkh(pk_compressed, self.network).to_string();
let p2pkh_uncompressed = Address::p2pkh(pk_uncompressed, self.network).to_string();

// P2WPKH (requires compressed pubkey)
// P2WPKH and P2SH-P2WPKH (both require compressed pubkey)
let compressed_pk = CompressedPublicKey::from_slice(&secp_pubkey.serialize())
.expect("valid compressed pubkey");
let p2sh_p2wpkh = Address::p2shwpkh(&compressed_pk, self.network).to_string();
let p2wpkh = Address::p2wpkh(&compressed_pk, self.network).to_string();

// P2TR (Taproot, key-path spend with no script tree)
Expand Down Expand Up @@ -167,6 +171,7 @@ impl KeyDeriver {
wif_uncompressed: priv_uncompressed.to_wif(),
p2pkh_compressed,
p2pkh_uncompressed,
p2sh_p2wpkh,
p2wpkh,
p2tr,
}
Expand Down Expand Up @@ -204,6 +209,7 @@ mod tests {
"1JwSSubhmg6iPtRjtyqhUYYH7bZg3Lfy1T"
);
assert!(derived.wif_compressed.starts_with('K') || derived.wif_compressed.starts_with('L'));
assert!(derived.p2sh_p2wpkh.starts_with('3'));
assert!(derived.p2wpkh.starts_with("bc1q"));
}

Expand All @@ -214,11 +220,12 @@ mod tests {
let derived = deriver.derive(&key);

let addrs = derived.addresses();
assert_eq!(addrs.len(), 4);
assert_eq!(addrs.len(), 5);
assert!(addrs[0].starts_with('1')); // P2PKH compressed
assert!(addrs[1].starts_with('1')); // P2PKH uncompressed
assert!(addrs[2].starts_with("bc1q")); // P2WPKH
assert!(addrs[3].starts_with("bc1p")); // P2TR
assert!(addrs[2].starts_with('3')); // P2SH-P2WPKH
assert!(addrs[3].starts_with("bc1q")); // P2WPKH
assert!(addrs[4].starts_with("bc1p")); // P2TR
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ fn run_single(passphrase: &str, transform_type: TransformType, network: &str) ->
println!("---");
println!("P2PKH (compressed): {}", derived.p2pkh_compressed);
println!("P2PKH (uncompressed): {}", derived.p2pkh_uncompressed);
println!("P2SH-P2WPKH: {}", derived.p2sh_p2wpkh);
println!("P2WPKH: {}", derived.p2wpkh);
println!("P2TR: {}", derived.p2tr);
}
Expand Down
26 changes: 26 additions & 0 deletions src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct MatchInfo {
pub enum AddressType {
P2pkhCompressed,
P2pkhUncompressed,
P2shP2wpkh,
P2wpkh,
P2tr,
}
Expand All @@ -31,6 +32,7 @@ impl AddressType {
match self {
AddressType::P2pkhCompressed => "p2pkh_compressed",
AddressType::P2pkhUncompressed => "p2pkh_uncompressed",
AddressType::P2shP2wpkh => "p2sh_p2wpkh",
AddressType::P2wpkh => "p2wpkh",
AddressType::P2tr => "p2tr",
}
Expand Down Expand Up @@ -88,6 +90,14 @@ impl Matcher {
});
}

// Check P2SH-P2WPKH
if self.targets.contains(&derived.p2sh_p2wpkh) {
return Some(MatchInfo {
address_type: AddressType::P2shP2wpkh,
address: derived.p2sh_p2wpkh.clone(),
});
}

// Check P2WPKH
if self.targets.contains(&derived.p2wpkh) {
return Some(MatchInfo {
Expand Down Expand Up @@ -163,6 +173,22 @@ mod tests {
assert!(info.address.starts_with("bc1p"));
}

#[test]
fn test_matcher_p2sh_p2wpkh() {
let key = [1u8; 32];
let deriver = KeyDeriver::new();
let derived = deriver.derive(&key);

let matcher = Matcher::from_addresses(vec![derived.p2sh_p2wpkh.clone()]);

let result = matcher.check(&derived);
assert!(result.is_some());

let info = result.unwrap();
assert!(matches!(info.address_type, AddressType::P2shP2wpkh));
assert!(info.address.starts_with('3'));
}

#[test]
fn test_matcher_no_match() {
let key = [1u8; 32];
Expand Down
4 changes: 4 additions & 0 deletions src/output/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl Output for ConsoleOutput {
writeln!(w, "wif_uncompressed: {}", derived.wif_uncompressed)?;
writeln!(w, "p2pkh_compressed: {}", derived.p2pkh_compressed)?;
writeln!(w, "p2pkh_uncompressed: {}", derived.p2pkh_uncompressed)?;
writeln!(w, "p2sh_p2wpkh: {}", derived.p2sh_p2wpkh)?;
writeln!(w, "p2wpkh: {}", derived.p2wpkh)?;
writeln!(w, "p2tr: {}", derived.p2tr)?;
} else {
Expand Down Expand Up @@ -115,6 +116,7 @@ impl Output for ConsoleOutput {
writeln!(w, "---")?;
writeln!(w, "P2PKH (compressed): {}", derived.p2pkh_compressed)?;
writeln!(w, "P2PKH (uncompressed): {}", derived.p2pkh_uncompressed)?;
writeln!(w, "P2SH-P2WPKH: {}", derived.p2sh_p2wpkh)?;
writeln!(w, "P2WPKH: {}", derived.p2wpkh)?;
writeln!(w, "P2TR: {}", derived.p2tr)?;
writeln!(w, "=========================")?;
Expand Down Expand Up @@ -166,6 +168,7 @@ mod tests {
wif_uncompressed: "WIF_U".to_string(),
p2pkh_compressed: "1Address".to_string(),
p2pkh_uncompressed: "1Uncompressed".to_string(),
p2sh_p2wpkh: "3Wrapped...".to_string(),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
p2wpkh: "bc1q...".to_string(),
p2tr: "bc1p...".to_string(),
}
Expand Down Expand Up @@ -242,5 +245,6 @@ mod tests {
assert!(content.contains("transform: sha256"));
assert!(content.contains("private_key: abc123"));
assert!(content.contains("p2pkh_compressed: 1Address"));
assert!(content.contains("p2sh_p2wpkh: 3Wrapped..."));
}
}
1 change: 1 addition & 0 deletions src/output/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ mod tests {
wif_uncompressed: "5J".to_string(),
p2pkh_compressed: "1ABC".to_string(),
p2pkh_uncompressed: "1DEF".to_string(),
p2sh_p2wpkh: "3GHI".to_string(),
p2wpkh: "bc1q".to_string(),
p2tr: "bc1p".to_string(),
}
Expand Down
5 changes: 5 additions & 0 deletions src/output/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ impl StorageOutput {
address_type: "p2pkh_uncompressed",
address: &derived.p2pkh_uncompressed,
},
AddressRecord {
address_type: "p2sh_p2wpkh",
address: &derived.p2sh_p2wpkh,
},
AddressRecord {
address_type: "p2wpkh",
address: &derived.p2wpkh,
Expand Down Expand Up @@ -279,6 +283,7 @@ mod tests {
wif_uncompressed: "5J1234567890".to_string(),
p2pkh_compressed: "1ABC123".to_string(),
p2pkh_uncompressed: "1DEF456".to_string(),
p2sh_p2wpkh: "3GHItest".to_string(),
p2wpkh: "bc1qtest".to_string(),
p2tr: "bc1ptest".to_string(),
}
Expand Down
2 changes: 2 additions & 0 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ fn verify_key_boha(key: &[u8; 32], query: &str) -> Result<VerifyReport> {
Some("p2pkh_compressed")
} else if derived.p2pkh_uncompressed == addr {
Some("p2pkh_uncompressed")
} else if derived.p2sh_p2wpkh == addr {
Some("p2sh_p2wpkh")
} else if derived.p2wpkh == addr {
Some("p2wpkh")
} else if derived.p2tr == addr {
Expand Down
8 changes: 7 additions & 1 deletion src/storage/iceberg/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ pub fn build_iceberg_schema() -> Result<IcebergSchema> {
Type::Primitive(PrimitiveType::String),
)
.into(),
NestedField::optional(
next_id(),
fields::ADDRESS_P2SH_P2WPKH,
Type::Primitive(PrimitiveType::String),
)
.into(),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
];

IcebergSchema::builder()
Expand All @@ -157,7 +163,7 @@ mod tests {
#[test]
fn build_schema_succeeds() {
let schema = build_iceberg_schema().unwrap();
assert_eq!(schema.as_struct().fields().len(), 20);
assert_eq!(schema.as_struct().fields().len(), 21);
}

#[test]
Expand Down
21 changes: 16 additions & 5 deletions src/storage/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod fields {
// Addresses
pub const ADDRESS_P2PKH_COMPRESSED: &str = "address_p2pkh_compressed";
pub const ADDRESS_P2PKH_UNCOMPRESSED: &str = "address_p2pkh_uncompressed";
pub const ADDRESS_P2SH_P2WPKH: &str = "address_p2sh_p2wpkh";
pub const ADDRESS_P2WPKH: &str = "address_p2wpkh";
pub const ADDRESS_P2TR: &str = "address_p2tr";

Expand All @@ -53,7 +54,7 @@ pub mod fields {
/// Variable-length fields (public_keys, addresses, export_formats) are mapped
/// to fixed columns based on known Bitcoin formats.
///
/// # Schema (20 columns)
/// # Schema (21 columns)
///
/// | Column | Type | Nullable | Description |
/// |--------|------|----------|-------------|
Expand All @@ -73,6 +74,7 @@ pub mod fields {
/// | pubkey_uncompressed | Utf8 | Yes | Uncompressed public key |
/// | address_p2pkh_compressed | Utf8 | Yes | P2PKH (compressed) |
/// | address_p2pkh_uncompressed | Utf8 | Yes | P2PKH (uncompressed) |
/// | address_p2sh_p2wpkh | Utf8 | Yes | P2SH-P2WPKH (wrapped segwit) |
/// | address_p2wpkh | Utf8 | Yes | P2WPKH (native segwit) |
/// | address_p2tr | Utf8 | Yes | P2TR (Taproot) |
/// | wif_compressed | Utf8 | Yes | WIF compressed |
Expand Down Expand Up @@ -107,6 +109,7 @@ pub fn result_schema() -> Schema {
// Addresses (nullable)
Field::new(fields::ADDRESS_P2PKH_COMPRESSED, DataType::Utf8, true),
Field::new(fields::ADDRESS_P2PKH_UNCOMPRESSED, DataType::Utf8, true),
Field::new(fields::ADDRESS_P2SH_P2WPKH, DataType::Utf8, true),
Field::new(fields::ADDRESS_P2WPKH, DataType::Utf8, true),
Field::new(fields::ADDRESS_P2TR, DataType::Utf8, true),
// Export formats (nullable)
Expand Down Expand Up @@ -229,6 +232,12 @@ pub fn records_to_batch(records: &[ResultRecord<'_>]) -> Result<RecordBatch, Arr
.map(|r| find_address(r.addresses, "p2pkh_uncompressed"))
.collect::<Vec<_>>(),
));
let address_p2sh_p2wpkh_array: ArrayRef = Arc::new(StringArray::from(
records
.iter()
.map(|r| find_address(r.addresses, "p2sh_p2wpkh"))
.collect::<Vec<_>>(),
));
let address_p2wpkh_array: ArrayRef = Arc::new(StringArray::from(
records
.iter()
Expand Down Expand Up @@ -275,6 +284,7 @@ pub fn records_to_batch(records: &[ResultRecord<'_>]) -> Result<RecordBatch, Arr
pubkey_uncompressed_array,
address_p2pkh_compressed_array,
address_p2pkh_uncompressed_array,
address_p2sh_p2wpkh_array,
address_p2wpkh_array,
address_p2tr_array,
wif_compressed_array,
Expand All @@ -291,9 +301,9 @@ mod tests {
use arrow::datatypes::DataType;

#[test]
fn schema_has_20_fields() {
fn schema_has_21_fields() {
let schema = result_schema();
assert_eq!(schema.fields().len(), 20);
assert_eq!(schema.fields().len(), 21);
}

#[test]
Expand All @@ -320,6 +330,7 @@ mod tests {
"pubkey_uncompressed",
"address_p2pkh_compressed",
"address_p2pkh_uncompressed",
"address_p2sh_p2wpkh",
"address_p2wpkh",
"address_p2tr",
"wif_compressed",
Expand Down Expand Up @@ -348,7 +359,7 @@ mod tests {
assert_eq!(schema.field(10).data_type(), &DataType::UInt16);
assert_eq!(schema.field(11).data_type(), &DataType::UInt8);

for i in 12..20 {
for i in 12..21 {
assert_eq!(schema.field(i).data_type(), &DataType::Utf8);
}
}
Expand All @@ -366,7 +377,7 @@ mod tests {
);
}

let nullable = [4, 12, 13, 14, 15, 16, 17, 18, 19];
let nullable = [4, 12, 13, 14, 15, 16, 17, 18, 19, 20];
for i in nullable {
assert!(
schema.field(i).is_nullable(),
Expand Down