Skip to content

Commit

Permalink
Merge pull request #1426 from KeystoneHQ/fractal-btc
Browse files Browse the repository at this point in the history
add fractal btc support
  • Loading branch information
soralit authored Oct 24, 2024
2 parents 5a9ec20 + 6a77507 commit f1e8a2c
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 16 deletions.
2 changes: 2 additions & 0 deletions rust/apps/bitcoin/src/addresses/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ impl FromStr for Address {

#[cfg(test)]
mod tests {
use crate::network::NetworkT;

use super::*;

#[test]
Expand Down
48 changes: 39 additions & 9 deletions rust/apps/bitcoin/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ use crate::errors::BitcoinError;
use alloc::string::{String, ToString};
use core::str::FromStr;

pub trait NetworkT {
fn get_unit(&self) -> String;
fn normalize(&self) -> String;
}

#[derive(Debug, Clone)]
pub enum Network {
Bitcoin,
Expand All @@ -11,8 +16,8 @@ pub enum Network {
BitcoinCash,
}

impl Network {
pub fn get_unit(&self) -> String {
impl NetworkT for Network {
fn get_unit(&self) -> String {
match self {
Network::Bitcoin => "BTC",
Network::BitcoinTestnet => "tBTC",
Expand All @@ -22,6 +27,20 @@ impl Network {
}
.to_string()
}

fn normalize(&self) -> String {
match self {
Network::Bitcoin => "Bitcoin Mainnet",
Network::BitcoinTestnet => "Bitcoin Testnet",
Network::Litecoin => "Litecoin",
Network::Dash => "Dash",
Network::BitcoinCash => "Bitcoin Cash",
}
.to_string()
}
}

impl Network {
pub fn bip44_coin_type(&self) -> String {
match self {
Network::Bitcoin => 0,
Expand Down Expand Up @@ -51,14 +70,25 @@ impl FromStr for Network {
}
}

impl Network {
pub fn normalize(&self) -> String {
#[derive(Debug, Clone, PartialEq)]
pub enum CustomNewNetwork {
FractalBitcoin,
FractalBitcoinTest,
}

impl NetworkT for CustomNewNetwork {
fn get_unit(&self) -> String {
match self {
Network::Bitcoin => "Bitcoin Mainnet",
Network::BitcoinTestnet => "Bitcoin Testnet",
Network::Litecoin => "Litecoin",
Network::Dash => "Dash",
Network::BitcoinCash => "Bitcoin Cash",
CustomNewNetwork::FractalBitcoin => "FB",
CustomNewNetwork::FractalBitcoinTest => "tFB",
}
.to_string()
}

fn normalize(&self) -> String {
match self {
CustomNewNetwork::FractalBitcoin => "Fractal Bitcoin",
CustomNewNetwork::FractalBitcoinTest => "Fractal Bitcoin Testnet",
}
.to_string()
}
Expand Down
2 changes: 1 addition & 1 deletion rust/apps/bitcoin/src/transactions/legacy/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use core::str::FromStr;
use crate::addresses::cashaddr::{Base58Codec, CashAddrCodec};
use crate::addresses::get_address;
use crate::errors::Result;
use crate::network::Network;
use crate::network::{Network, NetworkT};
use crate::transactions::legacy::input::TxIn;
use crate::transactions::legacy::output::TxOut;
use crate::transactions::legacy::tx_data::TxData;
Expand Down
2 changes: 1 addition & 1 deletion rust/apps/bitcoin/src/transactions/legacy/tx_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use third_party::bitcoin_hashes::{sha256, sha256d, Hash};

use crate::addresses::cashaddr::{Base58Codec, CashAddrCodec};
use crate::addresses::get_address;
use crate::network::Network;
use crate::network::{Network, NetworkT};
use crate::transactions::legacy::constants::{SIGHASH_ALL, SIGHASH_FORKID};
use crate::transactions::legacy::input::{InputConverter, TxIn};
use crate::transactions::legacy::output::{OutputConverter, TxOut};
Expand Down
6 changes: 3 additions & 3 deletions rust/apps/bitcoin/src/transactions/parsed_tx.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::errors::{BitcoinError, Result};
use crate::multi_sig::wallet::MultiSigWalletConfig;
use crate::network::Network;
use crate::network::{Network, NetworkT};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
Expand Down Expand Up @@ -90,7 +90,7 @@ impl ParseContext {
pub const DIVIDER: f64 = 100_000_000 as f64;

pub trait TxParser {
fn format_amount(value: u64, network: &Network) -> String {
fn format_amount(value: u64, network: &dyn NetworkT) -> String {
format!("{} {}", (value as f64).div(DIVIDER), network.get_unit())
}

Expand Down Expand Up @@ -146,7 +146,7 @@ pub trait TxParser {
&self,
inputs: Vec<ParsedInput>,
outputs: Vec<ParsedOutput>,
network: &Network,
network: &dyn NetworkT,
) -> Result<ParsedTx> {
let total_input_value = inputs.iter().fold(0, |acc, cur| acc + cur.value);
let total_output_value = outputs.iter().fold(0, |acc, cur| acc + cur.value);
Expand Down
52 changes: 51 additions & 1 deletion rust/apps/bitcoin/src/transactions/psbt/parsed_psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ impl TxParser for WrappedPsbt {
.enumerate()
.map(|(i, output)| self.parse_output(output, i, context, &network))
.collect::<Result<Vec<ParsedOutput>>>()?;
self.normalize(inputs, outputs, &network)

match self.identify_fractal_bitcoin_tx() {
Some(custom_net) => self.normalize(inputs, outputs, &custom_net),
None => self.normalize(inputs, outputs, &network),
}
}

fn determine_network(&self) -> Result<Network> {
if let Some((xpub, _)) = self.psbt.xpub.first_key_value() {
return if xpub.network == third_party::bitcoin::network::Network::Bitcoin {
Expand Down Expand Up @@ -134,6 +139,51 @@ mod tests {
assert_eq!(4000, first_output.value);
}

#[test]
fn test_parse_psbt_fb() {
let psbt_hex = "70736274ff01005202000000016d41e6873468f85aff76d7709a93b47180ea0784edaac748228d2c474396ca550000000000fdffffff01a00f0000000000001600146623828c1f87be7841a9b1cc360d38ae0a8b6ed0000000000961626364636861696e02666206ff636861696e037466620001011f6817000000000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca1220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a54000080010000800000008000000000000000000000";
let psbt = Psbt::deserialize(&Vec::from_hex(psbt_hex).unwrap()).unwrap();
let wpsbt = WrappedPsbt { psbt };
let master_fingerprint = Fingerprint::from_str("73c5da0a").unwrap();
let extended_pubkey = Xpub::from_str("xpub6Bm9M1SxZdzL3TxdNV8897FgtTLBgehR1wVNnMyJ5VLRK5n3tFqXxrCVnVQj4zooN4eFSkf6Sma84reWc5ZCXMxPbLXQs3BcaBdTd4YQa3B").unwrap();
let path = DerivationPath::from_str("m/84'/1'/0'").unwrap();
let mut keys = BTreeMap::new();
keys.insert(path, extended_pubkey);

let result = wpsbt
.parse(Some(&ParseContext {
master_fingerprint,
extended_public_keys: keys,
verify_code: None,
multisig_wallet_config: None,
}))
.unwrap();
assert_eq!("0.00005992 tFB", result.detail.total_input_amount);
assert_eq!("0.00004 tFB", result.detail.total_output_amount);
assert_eq!("0.00001992 tFB", result.detail.fee_amount);

assert_eq!("4000 sats", result.overview.total_output_sat);
assert_eq!("1992 sats", result.overview.fee_sat);

assert_eq!("Fractal Bitcoin Testnet", result.overview.network);

let first_input = result.detail.from.get(0).unwrap();
assert_eq!(
"tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl",
first_input.address.clone().unwrap()
);
assert_eq!(true, first_input.path.is_some());
assert_eq!(5992, first_input.value);

let first_output = result.detail.to.get(0).unwrap();
assert_eq!(
"tb1qvc3c9rqls7l8ssdfk8xrvrfc4c9gkmks87chvk",
first_output.address
);
assert_eq!(false, first_output.path.is_some());
assert_eq!(4000, first_output.value);
}

#[test]
fn test_parse_multisig_psbt() {
let psbt_hex = "70736274ff0100890200000001ed88352da4dca05f68c31cb05cf0940e2095b2fd602817d83ce0b2a6b0cd1eb80100000000fdffffff022d2a0000000000002200209dd6ca583cc39e08119cc73366d27e36bf9bba5f189ea697f1ebd8e04abd0690d00700000000000022002058f6dcd978c83e0bf741858f4c8e0d1212201074f02beb9b4ac4b2b75555718f01c80c004f010488b21e04b9124e6180000002cd32b21fd3718c3c08da48da5f2b44b00088dadf3179b8d7e0d23c63ad116d0e02fea2dd02ac2399962b4c0918aa8c21c3db988e9721ea425ecf00cc2705435b681452744703300000800000008000000080020000804f010488b21e045428576b8000000242ce3e3e0b5cd02d32c4b87816f1ae67e6b93c643750eb4fad4a264db872df9d02faa46048f4034e16d6cc1eaf687b3e21323d2359266bdd966e3eb5ca12b3eee51450659928300000800000008000000080020000804f010488b21e0481b9c1088000000253eaf13fdc5a252d0f66c061680e3302cddbfd00d2cd17ef7f2733edc6d66a5902b4cc2d00911da74b936b00f49b97c830b174d2b941b1894b1078b4ecec5d6e4f14515e384a300000800000008000000080020000800001012b78370000000000002200207bb3f49d46913a6e845e9b72ec167e60ab68a985c03fdf075f752175ef9e974101030401000000010569522102efafaa647275be895cfe637cb1e2b1eacde91196faf5c3e8a20b5a54b467253821031f47c601b25a6ebbbb6c87042d3888569270788095a16dd568970ca5cbd9befc2103d3eaaf547b30efabaeb20764d4747daec425b08c91205c3bb876b1a52175100e53ae220602efafaa647275be895cfe637cb1e2b1eacde91196faf5c3e8a20b5a54b46725381c52744703300000800000008000000080020000800000000000000000220603d3eaaf547b30efabaeb20764d4747daec425b08c91205c3bb876b1a52175100e1c506599283000008000000080000000800200008000000000000000002206031f47c601b25a6ebbbb6c87042d3888569270788095a16dd568970ca5cbd9befc1c515e384a3000008000000080000000800200008000000000000000000001016952210248dc844665aa6aafe1116bd9a754b98b9e888a33d36aececd30bbce267b3b2df2102bf4ce0d4ad56477221d9ac15f0bd1dd7fdc9d3ecee22749a9f5c19bc6baf29292103f87381ec21cbadfc089c2f0df9a0128f22b159af572bf61c1b76b6219f140e8153ae22020248dc844665aa6aafe1116bd9a754b98b9e888a33d36aececd30bbce267b3b2df1c52744703300000800000008000000080020000800100000000000000220203f87381ec21cbadfc089c2f0df9a0128f22b159af572bf61c1b76b6219f140e811c50659928300000800000008000000080020000800100000000000000220202bf4ce0d4ad56477221d9ac15f0bd1dd7fdc9d3ecee22749a9f5c19bc6baf29291c515e384a300000800000008000000080020000800100000000000000000101695221025489d7af6a4bb8b62cb7ad7f1c06368530e5ac31fe62f210f926fc2e027bfe7c210371eb571331a919b1b7a3a06348c6dc299c1e953ec39daee2912bc97f5b9fc4b22103f1bb9f033b28a98dbb82fe3a56c5b8c0317afb706d13d15591a3f12c7f65e4b753ae22020371eb571331a919b1b7a3a06348c6dc299c1e953ec39daee2912bc97f5b9fc4b21c527447033000008000000080000000800200008000000000010000002202025489d7af6a4bb8b62cb7ad7f1c06368530e5ac31fe62f210f926fc2e027bfe7c1c50659928300000800000008000000080020000800000000001000000220203f1bb9f033b28a98dbb82fe3a56c5b8c0317afb706d13d15591a3f12c7f65e4b71c515e384a30000080000000800000008002000080000000000100000000";
Expand Down
45 changes: 44 additions & 1 deletion rust/apps/bitcoin/src/transactions/psbt/wrapped_psbt.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use crate::errors::{BitcoinError, Result};
use alloc::format;

use third_party::bitcoin::hex::DisplayHex;
use third_party::bitcoin::script::Instruction;
use third_party::hex::ToHex;
use third_party::itertools::Itertools;

use crate::addresses::address::Address;
use crate::network;
use crate::network::{self, CustomNewNetwork};
use crate::transactions::parsed_tx::{ParseContext, ParsedInput, ParsedOutput, TxParser};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
Expand Down Expand Up @@ -819,6 +821,21 @@ impl WrappedPsbt {
}
return false;
}

// use global unknown for some custom usage
// current use it for identify whether it is the fractal bitcoin tx
pub fn identify_fractal_bitcoin_tx(&self) -> Option<CustomNewNetwork> {
self.psbt.unknown.iter().find_map(|(item_key, item_value)| {
(String::from_utf8(item_key.key.clone()).ok()? == "chain")
.then(|| String::from_utf8(item_value.clone()).ok())
.flatten()
.and_then(|value| match value.as_str() {
"fb" => Some(CustomNewNetwork::FractalBitcoin),
"tfb" => Some(CustomNewNetwork::FractalBitcoinTest),
_ => None,
})
})
}
}

fn derive_public_key_by_path(
Expand Down Expand Up @@ -871,6 +888,32 @@ mod tests {
}
}

#[test]
fn test_identify_fractal_bitcoin_tx() {
{
// no value in global
let psbt_hex = "70736274ff01005202000000016d41e6873468f85aff76d7709a93b47180ea0784edaac748228d2c474396ca550000000000fdffffff01a00f0000000000001600146623828c1f87be7841a9b1cc360d38ae0a8b6ed0000000000001011f6817000000000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca1220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a54000080010000800000008000000000000000000000";
let psbt = Psbt::deserialize(&Vec::from_hex(psbt_hex).unwrap()).unwrap();
let wrapper = WrappedPsbt { psbt };
let result = wrapper.identify_fractal_bitcoin_tx();
assert_eq!(result.is_none(), true);

// globalkey: ff chain fb (utf-8)
let psbt_hex = "70736274ff01005202000000016d41e6873468f85aff76d7709a93b47180ea0784edaac748228d2c474396ca550000000000fdffffff01a00f0000000000001600146623828c1f87be7841a9b1cc360d38ae0a8b6ed00000000006ff636861696e0266620001011f6817000000000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca1220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a54000080010000800000008000000000000000000000";
let psbt = Psbt::deserialize(&Vec::from_hex(psbt_hex).unwrap()).unwrap();
let wrapper = WrappedPsbt { psbt };
let result = wrapper.identify_fractal_bitcoin_tx().unwrap();
assert_eq!(result, CustomNewNetwork::FractalBitcoin);

// globalkey: ff chain tfb (utf-8)
let psbt_hex = "70736274ff01005202000000016d41e6873468f85aff76d7709a93b47180ea0784edaac748228d2c474396ca550000000000fdffffff01a00f0000000000001600146623828c1f87be7841a9b1cc360d38ae0a8b6ed00000000006ff636861696e037466620001011f6817000000000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca1220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a54000080010000800000008000000000000000000000";
let psbt = Psbt::deserialize(&Vec::from_hex(psbt_hex).unwrap()).unwrap();
let wrapper = WrappedPsbt { psbt };
let result = wrapper.identify_fractal_bitcoin_tx().unwrap();
assert_eq!(result, CustomNewNetwork::FractalBitcoinTest)
}
}

#[test]
fn test_taproot_sign() {
{
Expand Down

0 comments on commit f1e8a2c

Please sign in to comment.