diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5d434b4697b..4b778aab9e6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -164,9 +164,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b36f4c848f6bd9ff208128f08751135846cc23ae57d66ab10a22efff1c675f3c" +checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" dependencies = [ "bech32", "bitcoin-private", @@ -1485,6 +1485,7 @@ dependencies = [ name = "tw_any_coin" version = "0.1.0" dependencies = [ + "tw_any_coin", "tw_coin_entry", "tw_coin_registry", "tw_encoding", @@ -1503,10 +1504,14 @@ dependencies = [ "secp256k1", "serde", "serde_json", + "tw_coin_entry", "tw_encoding", + "tw_keypair", "tw_memory", "tw_misc", "tw_proto", + "tw_utxo", + "wallet-core-rs", ] [[package]] @@ -1528,6 +1533,7 @@ dependencies = [ "lazy_static", "serde", "serde_json", + "tw_bitcoin", "tw_coin_entry", "tw_ethereum", "tw_evm", @@ -1680,6 +1686,19 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_utxo" +version = "0.1.0" +dependencies = [ + "bitcoin", + "secp256k1", + "tw_coin_entry", + "tw_encoding", + "tw_keypair", + "tw_memory", + "tw_proto", +] + [[package]] name = "typenum" version = "1.16.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e5dabba1d6f..b8fdf347fca 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -15,6 +15,7 @@ members = [ "tw_number", "tw_proto", "tw_ronin", + "tw_utxo", "wallet_core_rs", ] diff --git a/rust/coverage.stats b/rust/coverage.stats index 2ac469b6906..a0db2948af6 100644 --- a/rust/coverage.stats +++ b/rust/coverage.stats @@ -1 +1 @@ -89.5 \ No newline at end of file +91.7 \ No newline at end of file diff --git a/rust/tw_any_coin/Cargo.toml b/rust/tw_any_coin/Cargo.toml index 50353ba28db..7ea537e73c2 100644 --- a/rust/tw_any_coin/Cargo.toml +++ b/rust/tw_any_coin/Cargo.toml @@ -14,6 +14,7 @@ tw_misc = { path = "../tw_misc" } test-utils = [] [dev-dependencies] +tw_any_coin = { path = "./", features = ["test-utils"] } tw_encoding = { path = "../tw_encoding" } tw_keypair = { path = "../tw_keypair", features = ["test-utils"] } tw_memory = { path = "../tw_memory", features = ["test-utils"] } diff --git a/rust/tw_any_coin/tests/tw_any_address_ffi_tests.rs b/rust/tw_any_coin/tests/tw_any_address_ffi_tests.rs index f4ce320d5fd..829e2b97af0 100644 --- a/rust/tw_any_coin/tests/tw_any_address_ffi_tests.rs +++ b/rust/tw_any_coin/tests/tw_any_address_ffi_tests.rs @@ -34,6 +34,8 @@ fn test_any_address_derive() { // TODO match `CoinType` when it's generated. let expected_address = match coin.blockchain { + // By default, Bitcoin will return a P2PKH address. + BlockchainType::Bitcoin => "19cAJn4Ms8jodBBGtroBNNpCZiHAWGAq7X", BlockchainType::Ethereum => "0xAc1ec44E4f0ca7D172B7803f6836De87Fb72b309", BlockchainType::Ronin => "ronin:Ac1ec44E4f0ca7D172B7803f6836De87Fb72b309", BlockchainType::Unsupported => unreachable!(), @@ -57,6 +59,10 @@ fn test_any_address_derive() { fn test_any_address_normalize_eth() { for coin in supported_coin_items() { let (denormalized, expected_normalized) = match coin.blockchain { + BlockchainType::Bitcoin => ( + "19cAJn4Ms8jodBBGtroBNNpCZiHAWGAq7X", + "19cAJn4Ms8jodBBGtroBNNpCZiHAWGAq7X", + ), BlockchainType::Ethereum => ( "0xb16db98b365b1f89191996942612b14f1da4bd5f", "0xb16Db98B365B1f89191996942612B14F1Da4Bd5f", @@ -85,9 +91,14 @@ fn test_any_address_normalize_eth() { } #[test] -fn test_any_address_is_valid_eth() { +fn test_any_address_is_valid_coin() { for coin in supported_coin_items() { let valid = match coin.blockchain { + BlockchainType::Bitcoin => vec![ + "1MrZNGN7mfWZiZNQttrzHjfw72jnJC2JNx", + "bc1qunq74p3h8425hr6wllevlvqqr6sezfxj262rff", + "bc1pwse34zfpvt344rvlt7tw0ngjtfh9xasc4q03avf0lk74jzjpzjuqaz7ks5", + ], BlockchainType::Ethereum => vec![ "0xb16db98b365b1f89191996942612b14f1da4bd5f", "0xb16Db98B365B1f89191996942612B14F1Da4Bd5f", @@ -98,7 +109,7 @@ fn test_any_address_is_valid_eth() { "ronin:b16db98b365b1f89191996942612b14f1da4bd5f", "ronin:b16Db98B365B1f89191996942612B14F1Da4Bd5f", ], - BlockchainType::Unsupported => unreachable!(), + _ => unreachable!(), }; for valid_addr in valid { @@ -109,9 +120,12 @@ fn test_any_address_is_valid_eth() { } #[test] -fn test_any_address_is_valid_eth_invalid() { +fn test_any_address_is_valid_coin_invalid() { for coin in supported_coin_items() { let invalid = match coin.blockchain { + BlockchainType::Bitcoin => { + vec!["0xb16db98b365b1f89191996942612b14f1da4bd5f"] + }, BlockchainType::Ethereum | BlockchainType::Ronin => { vec!["b16Db98B365B1f89191996942612B14F1Da4Bd5f"] }, diff --git a/rust/tw_bitcoin/Cargo.toml b/rust/tw_bitcoin/Cargo.toml index 82ef70900ab..24771b92ac7 100644 --- a/rust/tw_bitcoin/Cargo.toml +++ b/rust/tw_bitcoin/Cargo.toml @@ -10,7 +10,13 @@ bitcoin = "0.30.0" secp256k1 = { version = "0.27.0", features = [ "global-context", "rand-std" ] } serde = { version = "1.0.163", features = [ "derive" ] } serde_json = "1.0.96" +tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } +tw_utxo = { path = "../tw_utxo" } tw_encoding = { path = "../tw_encoding" } tw_memory = { path = "../tw_memory" } tw_misc = { path = "../tw_misc" } tw_proto = { path = "../tw_proto" } +tw_keypair = { path = "../tw_keypair" } + +[dev-dependencies] +wallet-core-rs = { path = "../wallet_core_rs" } diff --git a/rust/tw_bitcoin/src/brc20.rs b/rust/tw_bitcoin/src/brc20.rs deleted file mode 100644 index e2070059c2b..00000000000 --- a/rust/tw_bitcoin/src/brc20.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::ordinals::OrdinalsInscription; -use crate::{Error, Recipient, Result}; -use bitcoin::PublicKey; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BRC20Payload { - #[serde(rename = "p")] - protocol: String, - #[serde(rename = "op")] - operation: String, - #[serde(flatten)] - inner: T, -} - -impl BRC20Payload { - const PROTOCOL_ID: &str = "brc-20"; - const MIME: &[u8] = b"text/plain;charset=utf-8"; -} - -// Convenience aliases. -pub type BRC20DeployPayload = BRC20Payload; -pub type BRC20MintPayload = BRC20Payload; -pub type BRC20TransferPayload = BRC20Payload; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Ticker(String); - -impl Ticker { - pub fn new(string: String) -> Result { - // Ticker must be a 4-letter identifier. - if string.len() != 4 { - return Err(Error::Todo); - } - - Ok(Ticker(string)) - } - pub fn to_byte_array(&self) -> [u8; 4] { - self.0 - .as_bytes() - .try_into() - .expect("ticker must be four bytes") - } -} - -impl TryFrom for Ticker { - type Error = Error; - - fn try_from(string: String) -> Result { - Self::new(string) - } -} - -impl BRC20DeployPayload { - const OPERATION: &str = "deploy"; - - pub fn new(ticker: Ticker, max: usize, limit: Option, decimals: Option) -> Self { - BRC20Payload { - protocol: Self::PROTOCOL_ID.to_string(), - operation: Self::OPERATION.to_string(), - inner: DeployPayload { - tick: ticker, - max: max.to_string(), - lim: limit.map(|l| l.to_string()), - dec: decimals.map(|d| d.to_string()), - }, - } - } -} - -impl BRC20TransferPayload { - const OPERATION: &str = "transfer"; - - pub fn new(ticker: Ticker, amount: u64) -> Self { - BRC20Payload { - protocol: Self::PROTOCOL_ID.to_string(), - operation: Self::OPERATION.to_string(), - inner: TransferPayload { - tick: ticker, - amt: amount.to_string(), - }, - } - } -} - -impl BRC20MintPayload { - const OPERATION: &str = "mint"; - - pub fn new(ticker: Ticker, amount: u64) -> Self { - BRC20Payload { - protocol: Self::PROTOCOL_ID.to_string(), - operation: Self::OPERATION.to_string(), - inner: MintPayload { - tick: ticker, - amt: amount.to_string(), - }, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DeployPayload { - pub tick: Ticker, - pub max: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub lim: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dec: Option, -} - -#[derive(Debug, Clone)] -pub struct BRC20DeployInscription(OrdinalsInscription); - -impl BRC20DeployInscription { - pub fn new( - recipient: Recipient, - ticker: Ticker, - max: usize, - limit: Option, - decimals: Option, - ) -> Result { - let data = BRC20DeployPayload::new(ticker, max, limit, decimals); - - Self::from_payload(data, recipient) - } - pub fn from_payload( - data: BRC20DeployPayload, - recipient: Recipient, - ) -> Result { - let inscription = OrdinalsInscription::new( - BRC20Payload::::MIME, - &serde_json::to_vec(&data).unwrap(), - recipient, - )?; - - Ok(BRC20DeployInscription(inscription)) - } - pub fn inscription(&self) -> &OrdinalsInscription { - &self.0 - } -} - -#[derive(Serialize, Deserialize)] -pub struct TransferPayload { - pub tick: Ticker, - pub amt: String, -} - -pub struct BRC20TransferInscription(OrdinalsInscription); - -impl BRC20TransferInscription { - pub fn new( - recipient: Recipient, - ticker: Ticker, - amount: u64, - ) -> Result { - let data = BRC20TransferPayload::new(ticker, amount); - Self::from_payload(data, recipient) - } - pub fn from_payload( - data: BRC20TransferPayload, - recipient: Recipient, - ) -> Result { - let inscription = OrdinalsInscription::new( - BRC20Payload::::MIME, - &serde_json::to_vec(&data).unwrap(), - recipient, - )?; - - Ok(BRC20TransferInscription(inscription)) - } - pub fn inscription(&self) -> &OrdinalsInscription { - &self.0 - } -} - -/// The structure is the same as `TransferPayload`, but we'll keep it separate -/// for clarity. -#[derive(Serialize, Deserialize)] -pub struct MintPayload { - pub tick: Ticker, - pub amt: String, -} - -pub struct BRC20MintInscription(OrdinalsInscription); - -impl BRC20MintInscription { - pub fn new( - recipient: Recipient, - ticker: Ticker, - amount: u64, - ) -> Result { - let data = BRC20MintPayload::new(ticker, amount); - Self::from_payload(data, recipient) - } - pub fn from_payload( - data: BRC20MintPayload, - recipient: Recipient, - ) -> Result { - let inscription = OrdinalsInscription::new( - BRC20Payload::::MIME, - &serde_json::to_vec(&data).unwrap(), - recipient, - )?; - - Ok(BRC20MintInscription(inscription)) - } - pub fn inscription(&self) -> &OrdinalsInscription { - &self.0 - } -} diff --git a/rust/tw_bitcoin/src/claim.rs b/rust/tw_bitcoin/src/claim.rs deleted file mode 100644 index 02edc331ec5..00000000000 --- a/rust/tw_bitcoin/src/claim.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::{ - Error, Recipient, Result, TaprootScript, TxInputP2PKH, TxInputP2TRKeyPath, - TxInputP2TRScriptPath, TxInputP2WPKH, -}; -use bitcoin::key::{KeyPair, PublicKey, TapTweak, TweakedKeyPair, TweakedPublicKey}; -use bitcoin::secp256k1::Secp256k1; -use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; -use bitcoin::taproot::{LeafVersion, Signature}; -use bitcoin::{ScriptBuf, Witness}; - -#[derive(Debug, Clone)] -pub enum ClaimLocation { - Script(ScriptBuf), - Witness(Witness), -} - -pub trait TransactionSigner { - /// Claiming mechanism for (legacy) P2PKH outputs. - fn claim_p2pkh( - &self, - input: &TxInputP2PKH, - sighash: secp256k1::Message, - sighash_type: EcdsaSighashType, - ) -> Result; - /// Claiming mechanism for SegWit P2WPKH outputs. - fn claim_p2wpkh( - &self, - input: &TxInputP2WPKH, - sighash: secp256k1::Message, - sighash_type: EcdsaSighashType, - ) -> Result; - /// Claiming mechanism for Taproot P2TR key-path outputs. - fn claim_p2tr_key_path( - &self, - input: &TxInputP2TRKeyPath, - sighash: secp256k1::Message, - sighash_type: TapSighashType, - ) -> Result; - /// Claiming mechanism for Taproot P2TR script-path outputs. - fn claim_p2tr_script_path( - &self, - input: &TxInputP2TRScriptPath, - sighash: secp256k1::Message, - sighash_type: TapSighashType, - ) -> Result; -} - -// Contains the `scriptBuf` that must be included in the transaction when -// spending the P2PKH input. -pub struct ClaimP2PKH(pub ScriptBuf); - -// Contains the Witness that must be included in the transaction when spending -// the SegWit P2WPKH input. -pub struct ClaimP2WPKH(pub Witness); - -// Contains the Witness that must be included in the transaction when spending -// the Taproot P2TR key-path input. -pub struct ClaimP2TRKeyPath(pub Witness); - -// Contains the Witness that must be included in the transaction when spending -// the Taproot P2TR script-path input. -pub struct ClaimP2TRScriptPath(pub Witness); - -impl TransactionSigner for KeyPair { - fn claim_p2pkh( - &self, - input: &TxInputP2PKH, - sighash: secp256k1::Message, - sighash_type: EcdsaSighashType, - ) -> Result { - let me = Recipient::::from_keypair(self); - - // Check whether we can actually claim the input. - if input.recipient().pubkey_hash() != &me.pubkey_hash() { - return Err(Error::Todo); - } - - // Construct the ECDSA signature. - let sig = bitcoin::ecdsa::Signature { - sig: self.secret_key().sign_ecdsa(sighash), - hash_ty: sighash_type, - }; - - // Construct the Script for claiming. - let script = ScriptBuf::builder() - .push_slice(sig.serialize()) - .push_key(&me.public_key()) - .into_script(); - - Ok(ClaimP2PKH(script)) - } - fn claim_p2wpkh( - &self, - input: &TxInputP2WPKH, - sighash: secp256k1::Message, - sighash_type: EcdsaSighashType, - ) -> Result { - let me = Recipient::::from_keypair(self); - - if input.recipient().wpubkey_hash() != &me.wpubkey_hash()? { - return Err(Error::Todo); - } - - // Construct the ECDSA signature. - let sig = bitcoin::ecdsa::Signature { - sig: self.secret_key().sign_ecdsa(sighash), - hash_ty: sighash_type, - }; - - // Construct the Witness for claiming. - let mut witness = Witness::new(); - witness.push(sig.serialize()); - // Serialize public key. - witness.push(me.public_key().to_bytes()); - - Ok(ClaimP2WPKH(witness)) - } - fn claim_p2tr_key_path( - &self, - input: &TxInputP2TRKeyPath, - sighash: secp256k1::Message, - sighash_type: TapSighashType, - ) -> Result { - let me = Recipient::::from(self); - - // Check whether we can actually claim the input. - if input.recipient() != &me { - return Err(Error::Todo); - } - - let secp = Secp256k1::new(); - - // Tweak keypair for P2TR key-path (ie. zeroed Merkle root). - let tapped: TweakedKeyPair = self.tap_tweak(&secp, None); - let tweaked = KeyPair::from(tapped); - - // Construct the Schnorr signature. - #[cfg(not(test))] - let schnorr = secp.sign_schnorr(&sighash, &tweaked); - #[cfg(test)] - // For tests, we disable the included randomness in order to create - // reproducible signatures. Randomness should ALWAYS be used in - // production. - let schnorr = secp.sign_schnorr_no_aux_rand(&sighash, &tweaked); - - let sig = bitcoin::taproot::Signature { - sig: schnorr, - hash_ty: sighash_type, - }; - - // Construct the witness for claiming. - let mut witness = Witness::new(); - witness.push(sig.to_vec()); - - Ok(ClaimP2TRKeyPath(witness)) - } - fn claim_p2tr_script_path( - &self, - input: &TxInputP2TRScriptPath, - sighash: secp256k1::Message, - sighash_type: TapSighashType, - ) -> Result { - // Tweak our public key with the Merkle root of the Script to be claimed. - let me = Recipient::::from_keypair(self, input.recipient().merkle_root()); - - // Check whether we can actually claim the input. - if input.recipient() != &me { - return Err(Error::Todo); - } - - // The control block contains information on which script of the - // script-path is being executed. - let control_block = input - .spend_info() - .control_block(&(input.witness().clone(), LeafVersion::TapScript)) - .ok_or(Error::Todo)?; - - // Construct the Schnorr signature. We leave the keypair untweaked, - // unlike for key-path. - let sig = Signature { - sig: Secp256k1::new().sign_schnorr(&sighash, self), - hash_ty: sighash_type, - }; - - // Construct the Witness for claiming. - let mut witness = Witness::new(); - // Serialize signature. - witness.push(&sig.to_vec()); - witness.push(input.witness()); - witness.push(control_block.serialize()); - - Ok(ClaimP2TRScriptPath(witness)) - } -} diff --git a/rust/tw_bitcoin/src/entry.rs b/rust/tw_bitcoin/src/entry.rs new file mode 100644 index 00000000000..9700566d120 --- /dev/null +++ b/rust/tw_bitcoin/src/entry.rs @@ -0,0 +1,345 @@ +use crate::modules::plan_builder::BitcoinPlanBuilder; +use crate::modules::signer::Signer; +use crate::{Error, Result}; +use bitcoin::address::NetworkChecked; +use std::borrow::Cow; +use std::fmt::Display; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinAddress, CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::{AddressError, AddressResult}; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::prefix::NoPrefix; +use tw_coin_entry::signing_output_error; +use tw_keypair::tw::PublicKey; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +pub struct Address(pub bitcoin::address::Address); + +impl Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl CoinAddress for Address { + fn data(&self) -> tw_memory::Data { + self.0.to_string().into_bytes() + } +} + +pub struct BitcoinEntry; + +impl CoinEntry for BitcoinEntry { + type AddressPrefix = NoPrefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = Proto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = BitcoinPlanBuilder; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + let address = bitcoin::address::Address::from_str(address) + .map_err(|_| AddressError::FromHexError)? + .require_network(bitcoin::Network::Bitcoin) + .map_err(|_| AddressError::InvalidInput)?; + + Ok(Address(address)) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let pubkey = match public_key { + PublicKey::Secp256k1(pubkey) | PublicKey::Secp256k1Extended(pubkey) => pubkey, + _ => return Err(AddressError::InvalidInput), + }; + + let pubkey = bitcoin::PublicKey::from_slice(pubkey.to_vec().as_ref()) + .map_err(|_| AddressError::InvalidInput)?; + + let address: bitcoin::address::Address = bitcoin::address::Address::new( + bitcoin::Network::Bitcoin, + bitcoin::address::Payload::PubkeyHash(pubkey.pubkey_hash()), + ); + + Ok(Address(address)) + } + + #[inline] + fn sign(&self, _coin: &dyn CoinContext, proto: Self::SigningInput<'_>) -> Self::SigningOutput { + Signer::sign_proto(_coin, proto) + .unwrap_or_else(|err| signing_output_error!(Proto::SigningOutput, err)) + } + + #[inline] + fn preimage_hashes( + &self, + _coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + ) -> Self::PreSigningOutput { + self.preimage_hashes_impl(_coin, proto) + .unwrap_or_else(|err| signing_output_error!(Proto::PreSigningOutput, err)) + } + + #[inline] + fn compile( + &self, + _coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + signatures: Vec, + _public_keys: Vec, + ) -> Self::SigningOutput { + self.compile_impl(_coin, proto, signatures, _public_keys) + .unwrap_or_else(|err| signing_output_error!(Proto::SigningOutput, err)) + } + + #[inline] + fn plan_builder(&self) -> Option { + Some(BitcoinPlanBuilder) + } +} + +impl BitcoinEntry { + pub(crate) fn preimage_hashes_impl( + &self, + _coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + ) -> Result> { + let proto = pre_processor(proto); + + // Convert input builders into Utxo inputs. + let utxo_inputs = proto + .inputs + .iter() + .map(crate::modules::transactions::InputBuilder::utxo_from_proto) + .collect::>>()?; + + // Convert output builders into Utxo outputs. + let mut utxo_outputs = proto + .outputs + .iter() + .map(crate::modules::transactions::OutputBuilder::utxo_from_proto) + .collect::>>()?; + + // If automatic change output is enabled, a change script must be provided. + let change_script_pubkey = if proto.disable_change_output { + Cow::default() + } else { + // Convert output builder to Utxo output. + let output = crate::modules::transactions::OutputBuilder::utxo_from_proto( + &proto + .change_output + .ok_or_else(|| Error::from(Proto::Error::Error_invalid_change_output))?, + )?; + + output.script_pubkey + }; + + // Prepare SigningInput for Utxo sighash generation. + let utxo_signing = UtxoProto::SigningInput { + version: proto.version, + lock_time: proto.lock_time, + inputs: utxo_inputs.clone(), + outputs: utxo_outputs + .iter() + .map(|output| UtxoProto::TxOut { + value: output.value, + script_pubkey: Cow::Borrowed(&output.script_pubkey), + }) + .collect(), + input_selector: proto.input_selector, + weight_base: proto.fee_per_vb, + change_script_pubkey, + disable_change_output: proto.disable_change_output, + }; + + // Generate the sighashes to be signed. + let utxo_presigning = tw_utxo::compiler::Compiler::preimage_hashes(utxo_signing); + handle_utxo_error(&utxo_presigning.error)?; + + // If a change output was created by the Utxo compiler, we return it here too. + if utxo_presigning.outputs.len() == utxo_outputs.len() + 1 { + let change_output = utxo_presigning + .outputs + .last() + .expect("expected change output"); + + utxo_outputs.push(Proto::mod_PreSigningOutput::TxOut { + value: change_output.value, + script_pubkey: change_output.script_pubkey.to_vec().into(), + control_block: Default::default(), + taproot_payload: Default::default(), + }) + } + + Ok(Proto::PreSigningOutput { + error: Proto::Error::OK, + error_message: Default::default(), + txid: utxo_presigning.txid, + sighashes: utxo_presigning.sighashes, + // Update selected inputs. + utxo_inputs: utxo_presigning.inputs, + utxo_outputs, + weight_estimate: utxo_presigning.weight_estimate, + fee_estimate: utxo_presigning.fee_estimate, + }) + } + + pub(crate) fn compile_impl( + &self, + _coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + signatures: Vec, + _public_keys: Vec, + ) -> Result> { + let proto = pre_processor(proto); + + // There must be a signature for each input. + if proto.inputs.len() != signatures.len() { + return Err(Error::from( + Proto::Error::Error_unmatched_input_signature_count, + )); + } + + // Generate claims for all the inputs. + let mut utxo_input_claims: Vec = vec![]; + for (input, signature) in proto.inputs.iter().zip(signatures.into_iter()) { + let utxo_claim = + crate::modules::transactions::InputClaimBuilder::utxo_claim_from_proto( + input, signature, + )?; + utxo_input_claims.push(utxo_claim); + } + + // Process all the outputs. + let mut utxo_outputs = vec![]; + for output in proto.outputs { + let utxo = crate::modules::transactions::OutputBuilder::utxo_from_proto(&output)?; + + utxo_outputs.push(utxo); + } + + // Prepare PreSerialization input for Utxo compiler. + let utxo_preserializtion = UtxoProto::PreSerialization { + version: proto.version, + lock_time: proto.lock_time.clone(), + inputs: utxo_input_claims.clone(), + outputs: utxo_outputs + .iter() + .map(|out| UtxoProto::TxOut { + value: out.value, + script_pubkey: Cow::Borrowed(&out.script_pubkey), + }) + .collect(), + weight_base: proto.fee_per_vb, + }; + + // Compile the transaction, build the final encoded transaction + // containing the signatures/scriptSigs/witnesses. + let utxo_serialized = tw_utxo::compiler::Compiler::compile(utxo_preserializtion); + handle_utxo_error(&utxo_serialized.error)?; + + // Prepare `Proto::TransactionInput` protobufs for signing output. + let mut proto_inputs = vec![]; + for input in utxo_input_claims { + proto_inputs.push(Proto::TransactionInput { + txid: Cow::Owned(input.txid.to_vec()), + vout: input.vout, + sequence: input.sequence, + script_sig: Cow::Owned(input.script_sig.into_owned()), + witness_items: input + .witness_items + .into_iter() + .map(|item| Cow::Owned(item.into_owned())) + .collect(), + }); + } + + // Prepare `Proto::TransactionOutput` protobufs for output. + let mut proto_outputs = vec![]; + for output in utxo_outputs { + proto_outputs.push(Proto::TransactionOutput { + script_pubkey: output.script_pubkey, + value: output.value, + taproot_payload: output.taproot_payload, + control_block: output.control_block, + }); + } + + // Prepare `Proto::Transaction` protobuf for output. + let transaction = Proto::Transaction { + version: proto.version, + lock_time: proto.lock_time, + inputs: proto_inputs, + outputs: proto_outputs, + }; + + // Return the full protobuf output. + Ok(Proto::SigningOutput { + error: Proto::Error::OK, + error_message: Default::default(), + transaction: Some(transaction), + encoded: utxo_serialized.encoded, + txid: utxo_serialized.txid, + weight: utxo_serialized.weight, + fee: utxo_serialized.fee, + }) + } +} + +// Convenience function for pre-processing of certain fields that must be +// executed on each `CoinEntry` call. +pub(crate) fn pre_processor(mut proto: Proto::SigningInput<'_>) -> Proto::SigningInput<'_> { + // We automatically set the transaction version to 2. + if proto.version == 0 { + proto.version = 2; + } + + // If an input sequence (timelock, replace-by-fee, etc) of zero is not + // expliclity enabled, we interpreted a sequence of zero as the max value + // (default). + proto.inputs.iter_mut().for_each(|txin| { + if !txin.sequence_enable_zero && txin.sequence == 0 { + txin.sequence = u32::MAX + } + }); + + proto +} + +#[rustfmt::skip] +/// Convert `Utxo.proto` error type to `BitcoinV2.proto` error type. +fn handle_utxo_error(utxo_err: &UtxoProto::Error) -> Result<()> { + let bitcoin_err = match utxo_err { + UtxoProto::Error::OK => return Ok(()), + UtxoProto::Error::Error_invalid_leaf_hash => Proto::Error::Error_utxo_invalid_leaf_hash, + UtxoProto::Error::Error_invalid_sighash_type => Proto::Error::Error_utxo_invalid_sighash_type, + UtxoProto::Error::Error_invalid_lock_time => Proto::Error::Error_utxo_invalid_lock_time, + UtxoProto::Error::Error_invalid_txid => Proto::Error::Error_utxo_invalid_txid, + UtxoProto::Error::Error_sighash_failed => Proto::Error::Error_utxo_sighash_failed, + UtxoProto::Error::Error_missing_sighash_method => Proto::Error::Error_utxo_missing_sighash_method, + UtxoProto::Error::Error_failed_encoding => Proto::Error::Error_utxo_failed_encoding, + UtxoProto::Error::Error_insufficient_inputs => Proto::Error::Error_utxo_insufficient_inputs, + UtxoProto::Error::Error_missing_change_script_pubkey => Proto::Error::Error_utxo_missing_change_script_pubkey, + }; + + Err(Error::from(bitcoin_err)) +} diff --git a/rust/tw_bitcoin/src/ffi/address.rs b/rust/tw_bitcoin/src/ffi/address.rs deleted file mode 100644 index 63c34dc31fc..00000000000 --- a/rust/tw_bitcoin/src/ffi/address.rs +++ /dev/null @@ -1,98 +0,0 @@ -use super::CTaprootError; -use crate::Recipient; -use bitcoin::PublicKey; -use std::ffi::CString; -use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; -use tw_memory::ffi::c_result::CStrMutResult; - -#[no_mangle] -pub unsafe extern "C" fn tw_legacy_address_string( - pubkey: *const u8, - pubkey_len: usize, - network: Network, -) -> CStrMutResult { - // Convert Recipient. - let Some(slice) = CByteArrayRef::new(pubkey, pubkey_len).as_slice() else { - return CStrMutResult::error(CTaprootError::InvalidSlice); - }; - - let Ok(recipient) = Recipient::::from_slice(slice) else { - return CStrMutResult::error(CTaprootError::InvalidPubkey); - }; - - let address = recipient.legacy_address_string(network.into()); - let c_string = CString::new(address) - .expect("legacy address contains an internal 0 byte") - .into_raw(); - - CStrMutResult::ok(c_string) -} - -#[no_mangle] -pub unsafe extern "C" fn tw_segwit_address_string( - pubkey: *const u8, - pubkey_len: usize, - network: Network, -) -> CStrMutResult { - // Convert Recipient. - let Some(slice) = CByteArrayRef::new(pubkey, pubkey_len).as_slice() else { - return CStrMutResult::error(CTaprootError::InvalidSlice); - }; - - let Ok(recipient) = Recipient::::from_slice(slice) else { - return CStrMutResult::error(CTaprootError::InvalidPubkey); - }; - - let Ok(address) = recipient.segwit_address_string(network.into()) else { - return CStrMutResult::error(CTaprootError::InvalidSegwitPukey); - }; - - let c_string = CString::new(address) - .expect("legacy address contains an internal 0 byte") - .into_raw(); - - CStrMutResult::ok(c_string) -} - -#[no_mangle] -pub unsafe extern "C" fn tw_taproot_address_string( - pubkey: *const u8, - pubkey_len: usize, - network: Network, -) -> CStrMutResult { - // Convert Recipient. - let Some(slice) = CByteArrayRef::new(pubkey, pubkey_len).as_slice() else { - return CStrMutResult::error(CTaprootError::InvalidSlice); - }; - - let Ok(recipient) = Recipient::::from_slice(slice) else { - return CStrMutResult::error(CTaprootError::InvalidPubkey); - }; - - let address = recipient.taproot_address_string(network.into()); - let c_string = CString::new(address) - .expect("legacy address contains an internal 0 byte") - .into_raw(); - - CStrMutResult::ok(c_string) -} - -// A custom reimplementation of of `bitcoin::Network`. -#[repr(C)] -pub enum Network { - Bitcoin = 0, - Testnet = 1, - Signet = 2, - Regtest = 3, -} - -impl From for bitcoin::Network { - fn from(n: Network) -> Self { - match n { - Network::Bitcoin => bitcoin::Network::Bitcoin, - Network::Testnet => bitcoin::Network::Testnet, - Network::Signet => bitcoin::Network::Signet, - Network::Regtest => bitcoin::Network::Regtest, - } - } -} diff --git a/rust/tw_bitcoin/src/ffi/mod.rs b/rust/tw_bitcoin/src/ffi/mod.rs deleted file mode 100644 index 4b6f8988555..00000000000 --- a/rust/tw_bitcoin/src/ffi/mod.rs +++ /dev/null @@ -1,279 +0,0 @@ -#![allow(clippy::missing_safety_doc)] - -use crate::{ - calculate_fee, Error, Result, TXOutputP2TRScriptPath, TaprootScript, TxInputP2TRScriptPath, -}; -use bitcoin::{ - consensus::Decodable, - taproot::{NodeInfo, TapNodeHash, TaprootSpendInfo}, - PublicKey, ScriptBuf, Transaction, Txid, -}; -use secp256k1::hashes::Hash; -use secp256k1::KeyPair; -use std::borrow::Cow; -use tw_memory::ffi::c_byte_array::CByteArray; -use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; -use tw_memory::ffi::c_result::CUInt64Result; -use tw_memory::ffi::c_result::ErrorCode; -use tw_misc::try_or_else; -use tw_proto::Bitcoin::Proto::{ - OutPoint, SigningInput, SigningOutput, Transaction as ProtoTransaction, TransactionInput, - TransactionOutput, TransactionVariant as TrVariant, -}; - -pub mod address; -pub mod scripts; - -// Re-exports -pub use address::*; -pub use scripts::*; - -use crate::{ - Recipient, TransactionBuilder, TxInput, TxInputP2PKH, TxInputP2TRKeyPath, TxInputP2WPKH, - TxOutput, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH, -}; - -#[no_mangle] -pub unsafe extern "C" fn tw_bitcoin_calculate_transaction_fee( - input: *const u8, - input_len: usize, - sat_vb: u64, -) -> CUInt64Result { - let Some(mut encoded) = CByteArrayRef::new(input, input_len).as_slice() else { - return CUInt64Result::error(1); - }; - - // Decode transaction. - let Ok(tx) = Transaction::consensus_decode(&mut encoded) else { - return CUInt64Result::error(1); - }; - - // Calculate fee. - let (_weight, fee) = calculate_fee(&tx, sat_vb); - - CUInt64Result::ok(fee) -} - -#[no_mangle] -pub unsafe extern "C" fn tw_taproot_build_and_sign_transaction( - input: *const u8, - input_len: usize, -) -> CByteArray { - let data = CByteArrayRef::new(input, input_len) - .to_vec() - .unwrap_or_default(); - - let proto: SigningInput = try_or_else!(tw_proto::deserialize(&data), CByteArray::null); - let signing = try_or_else!(taproot_build_and_sign_transaction(proto), CByteArray::null); - - let serialized = tw_proto::serialize(&signing).expect("failed to serialize signed transaction"); - - CByteArray::from(serialized) -} - -/// Note: many of the fields used in the `SigningInput` are currently unused. We -/// can later easily replicate the funcationlity and behavior of the C++ -/// implemenation. -/// -/// Additionally, the `SigningInput` supports two ways of operating (which -/// should probably be separated anyway): one way where the `TransactionPlan` is -/// skipped (and hence automatically constructed) and the other way where the -/// `TransactionPlan` is created manually. As of now, it's expected that the -/// `TransactionPlan` is created manually, meaning that the caller must careful -/// construct the outputs, which must include the return/change transaction and -/// how much goes to the miner as fee ( minus -/// ). -pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result { - let privkey = proto.private_key.get(0).ok_or(Error::Todo)?; - - // Prepare keypair and derive corresponding public key. - let keypair = KeyPair::from_seckey_slice(&secp256k1::Secp256k1::new(), privkey.as_ref()) - .map_err(|_| crate::Error::Todo)?; - - let my_pubkey = Recipient::::from(keypair); - - let mut builder = TransactionBuilder::new(); - - // Process inputs. - for input in proto.utxo { - let my_pubkey = my_pubkey.clone(); - - let out_point = input.out_point.ok_or(Error::Todo)?; - let txid = Txid::from_slice(&out_point.hash).map_err(|_| crate::Error::Todo)?; - let vout = out_point.index; - let satoshis = input.amount as u64; - - let script_buf = ScriptBuf::from_bytes(input.script.to_vec()); - - let tx: TxInput = match input.variant { - TrVariant::P2PKH => { - TxInputP2PKH::new_with_script(txid, vout, my_pubkey.into(), satoshis, script_buf) - .into() - }, - TrVariant::P2WPKH => TxInputP2WPKH::new_with_script( - txid, - vout, - my_pubkey.try_into()?, - satoshis, - script_buf, - ) - .into(), - TrVariant::P2TRKEYPATH => TxInputP2TRKeyPath::new_with_script( - txid, - vout, - my_pubkey.into(), - satoshis, - script_buf, - ) - .into(), - TrVariant::BRC20TRANSFER | TrVariant::NFTINSCRIPTION => { - // We construct the merkle root for the given spending script. - let spending_script = ScriptBuf::from_bytes(input.spendingScript.to_vec()); - let merkle_root = TapNodeHash::from_script( - spending_script.as_script(), - bitcoin::taproot::LeafVersion::TapScript, - ); - - // Convert to tapscript recipient with the given merkle root. - let recipient = - Recipient::::from_pubkey_recipient(my_pubkey, merkle_root); - - // Derive the spending information for the taproot recipient. - let spend_info = TaprootSpendInfo::from_node_info( - &secp256k1::Secp256k1::new(), - recipient.untweaked_pubkey(), - NodeInfo::new_leaf_with_ver( - spending_script.clone(), - bitcoin::taproot::LeafVersion::TapScript, - ), - ); - - TxInputP2TRScriptPath::new_with_script( - txid, - vout, - recipient, - satoshis, - script_buf, - spending_script, - spend_info, - ) - .into() - }, - }; - - builder = builder.add_input(tx); - } - - // Process outputs. - for output in proto.plan.ok_or(Error::Todo)?.utxos { - let script_buf = ScriptBuf::from_bytes(output.script.to_vec()); - let satoshis = output.amount as u64; - - #[rustfmt::skip] - let tx: TxOutput = match output.variant { - TrVariant::P2PKH => { - TxOutputP2PKH::new_with_script(satoshis, script_buf).into() - }, - TrVariant::P2WPKH => { - TxOutputP2WPKH::new_with_script(satoshis, script_buf).into() - }, - TrVariant::P2TRKEYPATH => { - TxOutputP2TRKeyPath::new_with_script(satoshis, script_buf).into() - }, - // We're keeping those two variants separate for now, we're planning - // on writing a new interface as part of a larger task anyway. - TrVariant::BRC20TRANSFER => { - TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into() - }, - TrVariant::NFTINSCRIPTION => { - TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into() - } - }; - - builder = builder.add_output(tx); - } - - // Copy those values before `builder` gets consumed. - let version = builder.version; - let lock_time = builder.lock_time.to_consensus_u32(); - - // Sign transaction and create protobuf structures. - let tx = builder.sign_inputs(keypair)?; - - // Create Protobuf structures of inputs. - let mut proto_inputs = vec![]; - for input in &tx.inner.input { - let txid: Vec = input - .previous_output - .txid - .as_byte_array() - .iter() - .cloned() - .rev() - .collect(); - - proto_inputs.push(TransactionInput { - previousOutput: Some(OutPoint { - hash: Cow::from(txid), - index: input.previous_output.vout, - sequence: input.sequence.to_consensus_u32(), - // Unused. - tree: 0, - }), - sequence: input.sequence.to_consensus_u32(), - script: { - // If `scriptSig` is empty, then the Witness is being used. - if input.script_sig.is_empty() { - let witness: Vec = input.witness.to_vec().into_iter().flatten().collect(); - Cow::from(witness) - } else { - Cow::from(input.script_sig.to_bytes()) - } - }, - }); - } - - // Create Protobuf structures of outputs. - let mut proto_outputs = vec![]; - for output in &tx.inner.output { - proto_outputs.push(TransactionOutput { - value: output.value as i64, - script: Cow::from(output.script_pubkey.to_bytes()), - spendingScript: Cow::default(), - }) - } - - // Create Protobuf structure of the full transaction. - let mut signing = SigningOutput { - transaction: Some(ProtoTransaction { - version, - lockTime: lock_time, - inputs: proto_inputs, - outputs: proto_outputs, - }), - encoded: Cow::default(), - transaction_id: Cow::from(tx.inner.txid().to_string()), - error: tw_proto::Common::Proto::SigningError::OK, - error_message: Cow::default(), - }; - - // Sign transaction and update Protobuf structure. - let signed = tx.serialize()?; - signing.encoded = Cow::from(signed); - - Ok(signing) -} - -#[repr(C)] -pub enum CTaprootError { - Ok = 0, - InvalidSlice = 1, - InvalidPubkey = 2, - InvalidSegwitPukey = 3, -} - -impl From for ErrorCode { - fn from(error: CTaprootError) -> Self { - error as ErrorCode - } -} diff --git a/rust/tw_bitcoin/src/ffi/scripts.rs b/rust/tw_bitcoin/src/ffi/scripts.rs deleted file mode 100644 index f230cbd0cc0..00000000000 --- a/rust/tw_bitcoin/src/ffi/scripts.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::brc20::{BRC20TransferInscription, Ticker}; -use crate::nft::OrdinalNftInscription; -use crate::{ - Recipient, TXOutputP2TRScriptPath, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH, -}; -use bitcoin::{PublicKey, WPubkeyHash}; -use std::borrow::Cow; -use std::ffi::{c_char, CStr}; -use tw_memory::ffi::c_byte_array::CByteArray; -use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; -use tw_misc::try_or_else; -use tw_proto::Bitcoin::Proto::TransactionOutput; - -#[no_mangle] -// Builds the P2PKH scriptPubkey. -pub unsafe extern "C" fn tw_build_p2pkh_script( - satoshis: i64, - pubkey: *const u8, - pubkey_len: usize, -) -> CByteArray { - // Convert Recipient - let slice = try_or_else!( - CByteArrayRef::new(pubkey, pubkey_len).as_slice(), - CByteArray::null - ); - let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); - - let tx_out = TxOutputP2PKH::new(satoshis as u64, recipient); - - // Prepare and serialize protobuf structure. - let proto = TransactionOutput { - value: satoshis, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::default(), - }; - - let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); - - CByteArray::from(serialized) -} - -#[no_mangle] -// Builds the P2WPKH scriptPubkey. -pub unsafe extern "C" fn tw_build_p2wpkh_script( - satoshis: i64, - pubkey: *const u8, - pubkey_len: usize, -) -> CByteArray { - // Convert Recipient - let slice = try_or_else!( - CByteArrayRef::new(pubkey, pubkey_len).as_slice(), - CByteArray::null - ); - let recipient = try_or_else!( - Recipient::::from_slice(slice), - CByteArray::null - ); - - let tx_out = TxOutputP2WPKH::new(satoshis as u64, recipient); - - // Prepare and serialize protobuf structure. - let proto = TransactionOutput { - value: satoshis, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::default(), - }; - - let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); - - CByteArray::from(serialized) -} - -#[no_mangle] -// Builds the P2TR key-path scriptPubkey. -pub unsafe extern "C" fn tw_build_p2tr_key_path_script( - satoshis: i64, - pubkey: *const u8, - pubkey_len: usize, -) -> CByteArray { - // Convert Recipient - let slice = try_or_else!( - CByteArrayRef::new(pubkey, pubkey_len).as_slice(), - CByteArray::null - ); - let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); - - let tx_out = TxOutputP2TRKeyPath::new(satoshis as u64, recipient.into()); - - // Prepare and serialize protobuf structure. - let proto = TransactionOutput { - value: satoshis, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::default(), - }; - - let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); - - CByteArray::from(serialized) -} - -#[no_mangle] -// Builds the Ordinals inscripton for BRC20 transfer. -pub unsafe extern "C" fn tw_build_brc20_transfer_inscription( - // The 4-byte ticker. - ticker: *const c_char, - amount: u64, - satoshis: i64, - pubkey: *const u8, - pubkey_len: usize, -) -> CByteArray { - // Convert ticket. - let ticker = match CStr::from_ptr(ticker).to_str() { - Ok(input) => input, - Err(_) => return CByteArray::null(), - }; - - if ticker.len() != 4 { - return CByteArray::null(); - } - - let ticker = Ticker::new(ticker.to_string()).expect("ticker must be 4 bytes"); - - // Convert Recipient - let slice = try_or_else!( - CByteArrayRef::new(pubkey, pubkey_len).as_slice(), - CByteArray::null - ); - - let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); - - // Build transfer inscription. - let transfer = BRC20TransferInscription::new(recipient, ticker, amount) - .expect("transfer inscription implemented wrongly"); - - let tx_out = TXOutputP2TRScriptPath::new(satoshis as u64, transfer.inscription().recipient()); - let spending_script = transfer.inscription().taproot_program(); - - // Prepare and serialize protobuf structure. - let proto = TransactionOutput { - value: satoshis, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::from(spending_script.as_bytes()), - }; - - let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); - - CByteArray::from(serialized) -} - -#[no_mangle] -// Builds the Ordinals inscripton for BRC20 transfer. -pub unsafe extern "C" fn tw_bitcoin_build_nft_inscription( - mime_type: *const c_char, - data: *const u8, - data_len: usize, - satoshis: i64, - pubkey: *const u8, - pubkey_len: usize, -) -> CByteArray { - // Convert mimeType. - let mime_type = match CStr::from_ptr(mime_type).to_str() { - Ok(input) => input, - Err(_) => return CByteArray::null(), - }; - - // Convert data to inscribe. - let data = try_or_else!( - CByteArrayRef::new(data, data_len).as_slice(), - CByteArray::null - ); - - // Convert Recipient. - let slice = try_or_else!( - CByteArrayRef::new(pubkey, pubkey_len).as_slice(), - CByteArray::null - ); - - let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); - - // Inscribe NFT data. - let nft = OrdinalNftInscription::new(mime_type.as_bytes(), data, recipient) - .expect("Ordinal NFT inscription incorrectly constructed"); - - let tx_out = TXOutputP2TRScriptPath::new(satoshis as u64, nft.inscription().recipient()); - let spending_script = nft.inscription().taproot_program(); - - // Prepare and serialize protobuf structure. - let proto = TransactionOutput { - value: satoshis, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::from(spending_script.as_bytes()), - }; - - let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); - - CByteArray::from(serialized) -} diff --git a/rust/tw_bitcoin/src/input/mod.rs b/rust/tw_bitcoin/src/input/mod.rs deleted file mode 100644 index ebe68369c2d..00000000000 --- a/rust/tw_bitcoin/src/input/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Witness}; - -mod p2pkh; -mod p2tr_key_path; -mod p2tr_script_path; -mod p2wpkh; - -pub use p2pkh::*; -pub use p2tr_key_path::*; -pub use p2tr_script_path::*; -pub use p2wpkh::*; - -#[derive(Debug, Clone)] -pub struct InputContext { - pub previous_output: OutPoint, - pub value: u64, - // The condition for claiming the output. - pub script_pubkey: ScriptBuf, - pub sequence: Sequence, - // Witness data for Segwit/Taproot transactions. -} - -impl InputContext { - pub fn new(utxo: TxOut, point: OutPoint) -> Self { - InputContext { - previous_output: point, - value: utxo.value, - script_pubkey: utxo.script_pubkey, - // Default value of `0xFFFFFFFF = 4294967295`. - sequence: Sequence::default(), - } - } -} - -#[derive(Debug, Clone)] -pub enum TxInput { - P2PKH(TxInputP2PKH), - P2WPKH(TxInputP2WPKH), - P2TRKeyPath(TxInputP2TRKeyPath), - P2TRScriptPath(TxInputP2TRScriptPath), -} - -impl From for TxInput { - fn from(input: TxInputP2PKH) -> Self { - TxInput::P2PKH(input) - } -} - -impl From for TxInput { - fn from(input: TxInputP2WPKH) -> Self { - TxInput::P2WPKH(input) - } -} - -impl From for TxInput { - fn from(input: TxInputP2TRKeyPath) -> Self { - TxInput::P2TRKeyPath(input) - } -} - -impl From for TxInput { - fn from(input: TxInputP2TRScriptPath) -> Self { - TxInput::P2TRScriptPath(input) - } -} - -impl From for TxIn { - fn from(input: TxInput) -> Self { - let ctx = input.ctx(); - - TxIn { - previous_output: ctx.previous_output, - script_sig: ScriptBuf::new(), - sequence: ctx.sequence, - witness: Witness::default(), - } - } -} - -impl TxInput { - pub fn ctx(&self) -> &InputContext { - match self { - TxInput::P2PKH(t) => t.ctx(), - TxInput::P2WPKH(t) => t.ctx(), - TxInput::P2TRKeyPath(t) => t.ctx(), - TxInput::P2TRScriptPath(t) => t.ctx(), - } - } - pub fn satoshis(&self) -> u64 { - self.ctx().value - } -} diff --git a/rust/tw_bitcoin/src/input/p2pkh.rs b/rust/tw_bitcoin/src/input/p2pkh.rs deleted file mode 100644 index 66bfe3de73b..00000000000 --- a/rust/tw_bitcoin/src/input/p2pkh.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::{Error, InputContext, Recipient, Result}; -use bitcoin::{OutPoint, PubkeyHash, ScriptBuf, Sequence, Txid}; - -#[derive(Debug, Clone)] -pub struct TxInputP2PKH { - ctx: InputContext, - recipient: Recipient, -} - -impl TxInputP2PKH { - pub fn new(txid: Txid, vout: u32, recipient: Recipient, satoshis: u64) -> Self { - let script = ScriptBuf::new_p2pkh(recipient.pubkey_hash()); - Self::new_with_script(txid, vout, recipient, satoshis, script) - } - pub fn new_with_script( - txid: Txid, - vout: u32, - recipient: Recipient, - satoshis: u64, - script: ScriptBuf, - ) -> Self { - TxInputP2PKH { - ctx: InputContext { - previous_output: OutPoint { txid, vout }, - value: satoshis, - script_pubkey: script, - sequence: Sequence::default(), - }, - recipient, - } - } - pub fn builder() -> TxInputP2PKHBuilder { - TxInputP2PKHBuilder::new() - } - /// Read-only exposure to the context. - pub fn ctx(&self) -> &InputContext { - &self.ctx - } - /// Read-only exposure to the recipient. - pub fn recipient(&self) -> &Recipient { - &self.recipient - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxInputP2PKHBuilder { - txid: Option, - vout: Option, - recipient: Option>, - satoshis: Option, -} - -impl TxInputP2PKHBuilder { - pub fn new() -> TxInputP2PKHBuilder { - TxInputP2PKHBuilder { - txid: None, - vout: None, - recipient: None, - satoshis: None, - } - } - pub fn txid(mut self, txid: Txid) -> TxInputP2PKHBuilder { - self.txid = Some(txid); - self - } - pub fn vout(mut self, vout: u32) -> TxInputP2PKHBuilder { - self.vout = Some(vout); - self - } - pub fn recipient(mut self, recipient: impl Into>) -> TxInputP2PKHBuilder { - self.recipient = Some(recipient.into()); - self - } - pub fn satoshis(mut self, satoshis: u64) -> TxInputP2PKHBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn build(self) -> Result { - Ok(TxInputP2PKH::new( - self.txid.ok_or(Error::Todo)?, - self.vout.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - self.satoshis.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/input/p2tr_key_path.rs b/rust/tw_bitcoin/src/input/p2tr_key_path.rs deleted file mode 100644 index 29bf0ff07f5..00000000000 --- a/rust/tw_bitcoin/src/input/p2tr_key_path.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{Error, InputContext, Recipient, Result}; -use bitcoin::key::TweakedPublicKey; -use bitcoin::{OutPoint, ScriptBuf, Sequence, Txid}; - -#[derive(Debug, Clone)] -pub struct TxInputP2TRKeyPath { - ctx: InputContext, - recipient: Recipient, -} - -impl TxInputP2TRKeyPath { - pub fn new( - txid: Txid, - vout: u32, - recipient: Recipient, - satoshis: u64, - ) -> Self { - let script = ScriptBuf::new_v1_p2tr_tweaked(recipient.tweaked_pubkey()); - Self::new_with_script(txid, vout, recipient, satoshis, script) - } - pub fn new_with_script( - txid: Txid, - vout: u32, - recipient: Recipient, - satoshis: u64, - script: ScriptBuf, - ) -> Self { - TxInputP2TRKeyPath { - ctx: InputContext { - previous_output: OutPoint { txid, vout }, - value: satoshis, - script_pubkey: script, - sequence: Sequence::default(), - }, - recipient, - } - } - pub fn builder() -> TxInputP2TRKeyPathBuilder { - TxInputP2TRKeyPathBuilder::new() - } - /// Read-only exposure to the context. - pub fn ctx(&self) -> &InputContext { - &self.ctx - } - /// Read-only exposure to the recipient. - pub fn recipient(&self) -> &Recipient { - &self.recipient - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxInputP2TRKeyPathBuilder { - txid: Option, - vout: Option, - recipient: Option>, - satoshis: Option, -} - -impl TxInputP2TRKeyPathBuilder { - pub fn new() -> TxInputP2TRKeyPathBuilder { - Self::default() - } - pub fn txid(mut self, txid: Txid) -> TxInputP2TRKeyPathBuilder { - self.txid = Some(txid); - self - } - pub fn vout(mut self, vout: u32) -> TxInputP2TRKeyPathBuilder { - self.vout = Some(vout); - self - } - pub fn recipient( - mut self, - recipient: impl Into>, - ) -> TxInputP2TRKeyPathBuilder { - self.recipient = Some(recipient.into()); - self - } - pub fn satoshis(mut self, satoshis: u64) -> TxInputP2TRKeyPathBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn build(self) -> Result { - Ok(TxInputP2TRKeyPath::new( - self.txid.ok_or(Error::Todo)?, - self.vout.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - self.satoshis.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/input/p2tr_script_path.rs b/rust/tw_bitcoin/src/input/p2tr_script_path.rs deleted file mode 100644 index af02c107533..00000000000 --- a/rust/tw_bitcoin/src/input/p2tr_script_path.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::{Error, InputContext, Recipient, Result, TaprootScript}; -use bitcoin::script::ScriptBuf; -use bitcoin::taproot::TaprootSpendInfo; -use bitcoin::{OutPoint, Sequence, Txid}; - -#[derive(Debug, Clone)] -pub struct TxInputP2TRScriptPath { - ctx: InputContext, - recipient: Recipient, - witness: ScriptBuf, - spend_info: TaprootSpendInfo, -} - -impl TxInputP2TRScriptPath { - pub fn new( - txid: Txid, - vout: u32, - recipient: Recipient, - satoshis: u64, - witness: ScriptBuf, - spend_info: TaprootSpendInfo, - ) -> Self { - let script = ScriptBuf::new_v1_p2tr( - &secp256k1::Secp256k1::new(), - recipient.untweaked_pubkey(), - Some(recipient.merkle_root()), - ); - - Self::new_with_script(txid, vout, recipient, satoshis, script, witness, spend_info) - } - pub fn new_with_script( - txid: Txid, - vout: u32, - recipient: Recipient, - satoshis: u64, - script: ScriptBuf, - witness: ScriptBuf, - spend_info: TaprootSpendInfo, - ) -> Self { - TxInputP2TRScriptPath { - ctx: InputContext { - previous_output: OutPoint { txid, vout }, - value: satoshis, - script_pubkey: script, - sequence: Sequence::default(), - }, - recipient, - witness, - spend_info, - } - } - pub fn builder() -> TxInputP2TRScriptPathBuilder { - TxInputP2TRScriptPathBuilder::new() - } - pub fn ctx(&self) -> &InputContext { - &self.ctx - } - pub fn recipient(&self) -> &Recipient { - &self.recipient - } - pub fn witness(&self) -> &ScriptBuf { - &self.witness - } - pub fn spend_info(&self) -> &TaprootSpendInfo { - &self.spend_info - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxInputP2TRScriptPathBuilder { - txid: Option, - vout: Option, - recipient: Option>, - satoshis: Option, - script: Option, - spend_info: Option, -} - -impl TxInputP2TRScriptPathBuilder { - pub fn new() -> TxInputP2TRScriptPathBuilder { - TxInputP2TRScriptPathBuilder { - txid: None, - vout: None, - recipient: None, - satoshis: None, - script: None, - spend_info: None, - } - } - pub fn txid(mut self, txid: Txid) -> TxInputP2TRScriptPathBuilder { - self.txid = Some(txid); - self - } - pub fn vout(mut self, vout: u32) -> TxInputP2TRScriptPathBuilder { - self.vout = Some(vout); - self - } - pub fn recipient( - mut self, - recipient: Recipient, - ) -> TxInputP2TRScriptPathBuilder { - self.recipient = Some(recipient); - self - } - pub fn satoshis(mut self, satoshis: u64) -> TxInputP2TRScriptPathBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn script(mut self, script: ScriptBuf) -> TxInputP2TRScriptPathBuilder { - self.script = Some(script); - self - } - pub fn spend_info(mut self, spend_info: TaprootSpendInfo) -> TxInputP2TRScriptPathBuilder { - self.spend_info = Some(spend_info); - self - } - pub fn build(self) -> Result { - Ok(TxInputP2TRScriptPath::new( - self.txid.ok_or(Error::Todo)?, - self.vout.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - self.satoshis.ok_or(Error::Todo)?, - self.script.ok_or(Error::Todo)?, - self.spend_info.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/input/p2wpkh.rs b/rust/tw_bitcoin/src/input/p2wpkh.rs deleted file mode 100644 index 4270fe73688..00000000000 --- a/rust/tw_bitcoin/src/input/p2wpkh.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::{Error, InputContext, Recipient, Result}; -use bitcoin::{OutPoint, ScriptBuf, Sequence, Txid, WPubkeyHash}; - -#[derive(Debug, Clone)] -pub struct TxInputP2WPKH { - ctx: InputContext, - recipient: Recipient, -} - -impl TxInputP2WPKH { - pub fn new(txid: Txid, vout: u32, recipient: Recipient, satoshis: u64) -> Self { - let script = ScriptBuf::new_v0_p2wpkh(recipient.wpubkey_hash()); - Self::new_with_script(txid, vout, recipient, satoshis, script) - } - pub fn new_with_script( - txid: Txid, - vout: u32, - recipient: Recipient, - satoshis: u64, - script: ScriptBuf, - ) -> Self { - TxInputP2WPKH { - ctx: InputContext { - previous_output: OutPoint { txid, vout }, - value: satoshis, - script_pubkey: script, - sequence: Sequence::default(), - }, - recipient, - } - } - pub fn builder() -> TxInputP2WPKHBuilder { - TxInputP2WPKHBuilder::new() - } - /// Read-only exposure to the context. - pub fn ctx(&self) -> &InputContext { - &self.ctx - } - /// Read-only exposure to the recipient. - pub fn recipient(&self) -> &Recipient { - &self.recipient - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxInputP2WPKHBuilder { - txid: Option, - vout: Option, - recipient: Option>, - satoshis: Option, -} - -impl TxInputP2WPKHBuilder { - pub fn new() -> TxInputP2WPKHBuilder { - Self::default() - } - pub fn txid(mut self, txid: Txid) -> TxInputP2WPKHBuilder { - self.txid = Some(txid); - self - } - pub fn vout(mut self, vout: u32) -> TxInputP2WPKHBuilder { - self.vout = Some(vout); - self - } - pub fn recipient(mut self, recipient: Recipient) -> TxInputP2WPKHBuilder { - self.recipient = Some(recipient); - self - } - pub fn satoshis(mut self, satoshis: u64) -> TxInputP2WPKHBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn build(self) -> Result { - Ok(TxInputP2WPKH::new( - self.txid.ok_or(Error::Todo)?, - self.vout.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - self.satoshis.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/lib.rs b/rust/tw_bitcoin/src/lib.rs index 7ff788a5dc6..a3bb3d0ac69 100644 --- a/rust/tw_bitcoin/src/lib.rs +++ b/rust/tw_bitcoin/src/lib.rs @@ -1,28 +1,68 @@ extern crate serde; -pub mod brc20; -pub mod claim; -pub mod ffi; -pub mod input; -pub mod nft; -pub mod ordinals; -pub mod output; -pub mod recipient; -#[cfg(test)] -mod tests; -pub mod transaction; -pub mod utils; - -// Reexports -pub use input::*; -pub use output::*; -pub use recipient::Recipient; -pub use transaction::*; -pub use utils::*; +pub mod entry; +pub mod modules; + +use std::fmt::Display; + +pub use bitcoin as native; +pub use entry::*; +pub use secp256k1; + +use tw_proto::BitcoinV2::Proto; pub type Result = std::result::Result; -#[derive(Debug, Clone)] -pub enum Error { - Todo, +#[derive(Debug)] +pub struct Error(Proto::Error); + +// TODO: We can improve this. +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl From for Error { + fn from(value: Proto::Error) -> Self { + Error(value) + } +} + +impl From for Proto::Error { + fn from(value: Error) -> Self { + value.0 + } +} + +impl From for Error { + fn from(_: bitcoin::key::Error) -> Self { + Error::from(Proto::Error::Error_invalid_public_key) + } +} + +impl From for Error { + fn from(_: bitcoin::ecdsa::Error) -> Self { + Error::from(Proto::Error::Error_invalid_ecdsa_signature) + } +} + +impl From for Error { + fn from(_: bitcoin::taproot::Error) -> Self { + Error::from(Proto::Error::Error_invalid_schnorr_signature) + } +} + +// Convenience aliases. +#[rustfmt::skip] +pub mod aliases { + use super::Proto; + + pub type ProtoOutputRecipient<'a> = Proto::mod_Output::OneOfto_recipient<'a>; + pub type ProtoOutputBuilder<'a> = Proto::mod_Output::mod_OutputBuilder::OneOfvariant<'a>; + pub type ProtoOutputRedeemScriptOrHashBuilder<'a> = Proto::mod_Output::mod_OutputRedeemScriptOrHash::OneOfvariant<'a>; + pub type ProtoPubkeyOrHash<'a> = Proto::mod_ToPublicKeyOrHash::OneOfto_address<'a>; + pub type ProtoRedeemScriptOrHash<'a> = Proto::mod_Output::mod_OutputRedeemScriptOrHash::OneOfvariant<'a>; + pub type ProtoInputRecipient<'a> = Proto::mod_Input::OneOfto_recipient<'a>; + pub type ProtoInputBuilder<'a> = Proto::mod_Input::mod_InputBuilder::OneOfvariant<'a>; } diff --git a/rust/tw_bitcoin/src/modules/legacy/build_and_sign.rs b/rust/tw_bitcoin/src/modules/legacy/build_and_sign.rs new file mode 100644 index 00000000000..04cbe0d8d92 --- /dev/null +++ b/rust/tw_bitcoin/src/modules/legacy/build_and_sign.rs @@ -0,0 +1,243 @@ +use crate::aliases::*; +use crate::{Error, Result}; +use bitcoin::absolute::LockTime; +use bitcoin::taproot::{LeafVersion, NodeInfo, TaprootSpendInfo}; +use bitcoin::{Network, PrivateKey, PublicKey, ScriptBuf}; +use secp256k1::XOnlyPublicKey; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_encoding::hex; +use tw_misc::traits::ToBytesVec; +use tw_proto::Bitcoin::Proto as LegacyProto; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Common::Proto as CommonProto; +use tw_proto::Utxo::Proto as UtxoProto; + +// Builds a Taproot transaction for the legacy protobuf structure, as used by +// `tw_bitcoin_legacy_taproot_build_and_sign_transaction` in the +// `wallet-core-rs` crate. +pub fn taproot_build_and_sign_transaction( + legacy: LegacyProto::SigningInput, +) -> Result { + // Convert the appropriate lock time. + let native_lock_time = LockTime::from_consensus(legacy.lock_time); + let lock_time = match native_lock_time { + LockTime::Blocks(blocks) => UtxoProto::LockTime { + variant: UtxoProto::mod_LockTime::OneOfvariant::blocks(blocks.to_consensus_u32()), + }, + LockTime::Seconds(seconds) => UtxoProto::LockTime { + variant: UtxoProto::mod_LockTime::OneOfvariant::seconds(seconds.to_consensus_u32()), + }, + }; + + // Prepare the inputs. + let mut inputs = vec![]; + for (index, utxo) in legacy.utxo.iter().enumerate() { + // We try to fetch the private key in the `private_key` fields by the + // corresponding index. If there is none, we default to the first + // provided key (implying that one single private key is used for all + // inputs). + let private_key = if let Some(private_key) = legacy.private_key.get(index) { + private_key + } else { + legacy + .private_key + .get(0) + .ok_or_else(|| Error::from(Proto::Error::Error_legacy_no_private_key))? + }; + + let private_key = PrivateKey::from_slice(private_key, Network::Bitcoin)?; + let my_pubkey = private_key.public_key(&secp256k1::Secp256k1::new()); + + let mut input = input_from_legacy_utxo(my_pubkey, utxo, legacy.hash_type)?; + input.private_key = private_key.to_bytes().into(); + inputs.push(input); + } + + // We skip any sort of builders and use the provided scripts directly. + let mut outputs = vec![]; + for output in legacy + .plan + .ok_or_else(|| Error::from(Proto::Error::Error_legacy_no_plan_provided))? + .utxos + { + outputs.push(Proto::Output { + value: output.amount as u64, + to_recipient: ProtoOutputRecipient::custom_script_pubkey(output.script), + }) + } + + // We only select enough inputs to cover the output balance. However, since + // some transaction types require precise input ordering (such as BRC20), we + // do not sort the inputs and use the ordering as provided by the caller. + let input_selector = UtxoProto::InputSelector::SelectInOrder; + + // The primary payload. + let signing_input = Proto::SigningInput { + version: 2, + private_key: legacy + .private_key + .get(0) + .map(|pk| pk.to_vec().into()) + .unwrap_or_default(), + lock_time: Some(lock_time), + inputs, + outputs, + input_selector, + fee_per_vb: legacy.byte_fee as u64, + change_output: None, + disable_change_output: true, + dangerous_use_fixed_schnorr_rng: false, + }; + + // Build and sign the Bitcoin transaction. + let signed = crate::entry::BitcoinEntry.sign(&EmptyCoinContext, signing_input); + + // Check for error. + if signed.error != Proto::Error::OK { + return Err(Error::from(signed.error)); + } + + let transaction = signed + .transaction + .expect("transaction not returned from signer"); + + // Convert the returned transaction data into the (legacy) `Transaction` + // protobuf from `Bitcoin.proto`. + let legacy_transaction = LegacyProto::Transaction { + version: 2, + lockTime: native_lock_time.to_consensus_u32(), + inputs: transaction + .inputs + .iter() + .map(|input| LegacyProto::TransactionInput { + previousOutput: Some(LegacyProto::OutPoint { + hash: input.txid.clone(), + index: input.vout, + sequence: input.sequence, + // Unused for Bitcoin + tree: Default::default(), + }), + // Notr: Not sure why this exists twice? + sequence: input.sequence, + script: input.script_sig.clone(), + }) + .collect(), + outputs: transaction + .outputs + .iter() + .map(|output| LegacyProto::TransactionOutput { + value: output.value as i64, + script: output.script_pubkey.clone(), + spendingScript: output.taproot_payload.clone(), + }) + .collect(), + }; + + let txid_hex = hex::encode(signed.txid.as_ref(), false); + + // Put the `Transaction` into the `SigningOutput`, return. + let legacy_output = LegacyProto::SigningOutput { + transaction: Some(legacy_transaction), + encoded: signed.encoded, + transaction_id: txid_hex.into(), + error: CommonProto::SigningError::OK, + error_message: Default::default(), + }; + + Ok(legacy_output) +} + +/// Convenience function. +fn input_from_legacy_utxo( + my_pubkey: PublicKey, + utxo: &LegacyProto::UnspentTransaction, + hash_type: u32, +) -> Result> { + // We identify the provided `Variant` and invoke the builder function. We + // explicitly skip/ignore any provided script in the input. + let input_builder = match utxo.variant { + LegacyProto::TransactionVariant::P2PKH => Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(my_pubkey.to_bytes().into()), + }, + LegacyProto::TransactionVariant::P2WPKH => Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(my_pubkey.to_bytes().into()), + }, + LegacyProto::TransactionVariant::P2TRKEYPATH => Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2tr_key_path(Proto::mod_Input::InputTaprootKeyPath { + one_prevout: false, + public_key: my_pubkey.to_bytes().into(), + }), + }, + LegacyProto::TransactionVariant::BRC20TRANSFER + | LegacyProto::TransactionVariant::NFTINSCRIPTION => { + // The spending script must to be empty. + if utxo.spendingScript.is_empty() { + return Err(Error::from( + Proto::Error::Error_legacy_no_spending_script_provided, + )); + } + + let spending_script = ScriptBuf::from_bytes(utxo.spendingScript.to_vec()); + + let xonly = XOnlyPublicKey::from(my_pubkey.inner); + let spend_info = TaprootSpendInfo::from_node_info( + &secp256k1::Secp256k1::new(), + xonly, + NodeInfo::new_leaf_with_ver(spending_script.clone(), LeafVersion::TapScript), + ); + + let control_block = spend_info + .control_block(&(spending_script, LeafVersion::TapScript)) + .expect("failed to construct control block"); + + Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2tr_script_path( + Proto::mod_Input::InputTaprootScriptPath { + one_prevout: false, + payload: utxo.spendingScript.to_vec().into(), + control_block: control_block.serialize().into(), + }, + ), + } + }, + }; + + // Convert the integer indicating the sighash type into the corresponding + // Utxo variant. + let sighash_type = match hash_type { + 0 => UtxoProto::SighashType::UseDefault, + 1 => UtxoProto::SighashType::All, + 2 => UtxoProto::SighashType::None_pb, + 3 => UtxoProto::SighashType::Single, + 129 => UtxoProto::SighashType::AllPlusAnyoneCanPay, + 130 => UtxoProto::SighashType::NonePlusAnyoneCanPay, + 131 => UtxoProto::SighashType::SinglePlusAnyoneCanPay, + _ => return Err(Error::from(Proto::Error::Error_utxo_invalid_sighash_type)), + }; + + // Construct Input and return. + let out_point = utxo + .out_point + .as_ref() + .ok_or_else(|| Error::from(Proto::Error::Error_legacy_outpoint_not_set))?; + + // We explicitly disable zero-valued sequences for legacy and default to + // `0xFFFFFFFF`' + let sequence = if out_point.sequence == 0 { + u32::MAX + } else { + out_point.sequence + }; + + Ok(Proto::Input { + private_key: Default::default(), + txid: out_point.hash.to_vec().into(), + vout: out_point.index, + value: utxo.amount as u64, + sequence, + sequence_enable_zero: false, + sighash_type, + to_recipient: ProtoInputRecipient::builder(input_builder), + }) +} diff --git a/rust/tw_bitcoin/src/modules/legacy/mod.rs b/rust/tw_bitcoin/src/modules/legacy/mod.rs new file mode 100644 index 00000000000..b488c44f4a6 --- /dev/null +++ b/rust/tw_bitcoin/src/modules/legacy/mod.rs @@ -0,0 +1,6 @@ +#![allow(clippy::missing_safety_doc)] + +pub mod build_and_sign; + +// Re-exports +pub use build_and_sign::*; diff --git a/rust/tw_bitcoin/src/modules/mod.rs b/rust/tw_bitcoin/src/modules/mod.rs new file mode 100644 index 00000000000..ff6024a4895 --- /dev/null +++ b/rust/tw_bitcoin/src/modules/mod.rs @@ -0,0 +1,5 @@ +pub mod legacy; +pub mod plan_builder; +pub mod signer; +pub mod transactions; +mod utils; diff --git a/rust/tw_bitcoin/src/modules/plan_builder.rs b/rust/tw_bitcoin/src/modules/plan_builder.rs new file mode 100644 index 00000000000..fd8eb880cad --- /dev/null +++ b/rust/tw_bitcoin/src/modules/plan_builder.rs @@ -0,0 +1,230 @@ +use crate::modules::utils::hard_clone_proto_output; +use crate::{aliases::*, pre_processor, BitcoinEntry}; +use crate::{Error, Result}; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::modules::plan_builder::PlanBuilder; +use tw_coin_entry::signing_output_error; +use tw_proto::BitcoinV2::Proto; +use tw_proto::BitcoinV2::Proto::mod_Input::InputBrc20Inscription; +use tw_proto::Utxo::Proto as UtxoProto; + +pub struct BitcoinPlanBuilder; + +impl PlanBuilder for BitcoinPlanBuilder { + type SigningInput<'a> = Proto::ComposePlan<'a>; + type Plan = Proto::TransactionPlan<'static>; + + #[inline] + fn plan( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + proto: Self::SigningInput<'_>, + ) -> Self::Plan { + self.plan_impl(_coin, proto) + .unwrap_or_else(|err| signing_output_error!(Proto::TransactionPlan, err)) + } +} + +impl BitcoinPlanBuilder { + fn plan_impl( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + proto: Proto::ComposePlan<'_>, + ) -> Result> { + let plan = match proto.compose { + Proto::mod_ComposePlan::OneOfcompose::brc20(plan) => { + let built_plan = self.plan_brc20(_coin, plan)?; + + Proto::TransactionPlan { + error: Proto::Error::OK, + error_message: Default::default(), + plan: Proto::mod_TransactionPlan::OneOfplan::brc20(built_plan), + } + }, + _ => panic!(), + }; + + Ok(plan) + } + fn plan_brc20( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + proto: Proto::mod_ComposePlan::ComposeBrc20Plan<'_>, + ) -> Result> { + // Hard-clones + let inscription = proto + .inscription + .ok_or_else(|| Error::from(Proto::Error::Error_missing_inscription))?; + + let brc20_info = InputBrc20Inscription { + one_prevout: inscription.one_prevout, + inscribe_to: inscription.inscribe_to.to_vec().into(), + ticker: inscription.ticker.to_string().into(), + transfer_amount: inscription.transfer_amount, + }; + + let tagged_output = super::utils::hard_clone_proto_output( + proto + .tagged_output + .ok_or_else(|| Error::from(Proto::Error::Error_missing_tagged_output))?, + )?; + + // First, we create the reveal transaction in order to calculate its input requirement (fee + dust limit). + + // We can use a zeroed Txid here. + let txid = vec![0; 32]; + let dummy_brc20_input = Proto::Input { + txid: txid.into(), + // The value is not relevant here, but we raise it above the output + // or we get an error. + value: u64::MAX, + sighash_type: UtxoProto::SighashType::UseDefault, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::brc20_inscribe(brc20_info.clone()), + }), + ..Default::default() + }; + + let dummy_reveal = Proto::SigningInput { + inputs: vec![dummy_brc20_input], + outputs: vec![tagged_output.clone()], + input_selector: UtxoProto::InputSelector::UseAll, + // Disable change output creation. + fee_per_vb: proto.fee_per_vb, + disable_change_output: true, + ..Default::default() + }; + + // We can now determine the fee of the reveal transaction. + let dummy_presigned = BitcoinEntry.preimage_hashes(_coin, dummy_reveal); + if dummy_presigned.error != Proto::Error::OK { + return Err(Error::from(dummy_presigned.error)); + } + + assert_eq!(dummy_presigned.error, Proto::Error::OK); + let reveal_fee_estimate = dummy_presigned.fee_estimate; + + // Create the BRC20 output for the COMMIT transaction; we set the + // amount to the estimated fee (REVEAL) plus the dust limit (`tagged_output.value`). + let brc20_output = Proto::Output { + value: reveal_fee_estimate + tagged_output.value, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::brc20_inscribe( + Proto::mod_Output::OutputBrc20Inscription { + inscribe_to: brc20_info.inscribe_to.to_vec().into(), + ticker: brc20_info.ticker.to_string().into(), + transfer_amount: brc20_info.transfer_amount, + }, + ), + }), + }; + + let brc20_output_value = brc20_output.value; + + // Clone the change output, if provided. + let change_output = if let Some(change) = proto.change_output { + Some(super::utils::hard_clone_proto_output(change)?) + } else { + None + }; + + // Create the full COMMIT transaction with the appropriately selected inputs. + let commit_signing = Proto::SigningInput { + private_key: proto.private_key.to_vec().into(), + inputs: proto + .inputs + .iter() + .cloned() + .map(super::utils::hard_clone_proto_input) + .collect::>()?, + outputs: vec![brc20_output], + input_selector: proto.input_selector, + change_output: change_output.clone(), + fee_per_vb: proto.fee_per_vb, + disable_change_output: proto.disable_change_output, + ..Default::default() + }; + + let mut commit_signing = pre_processor(commit_signing); + + // We now determine the Txid of the COMMIT transaction, which we will have + // to use in the REVEAL transaction. + let presigned = BitcoinEntry.preimage_hashes(_coin, commit_signing.clone()); + if presigned.error != Proto::Error::OK { + return Err(Error::from(presigned.error)); + } + + assert_eq!(presigned.error, Proto::Error::OK); + let commit_txid: Vec = presigned.txid.to_vec().iter().copied().rev().collect(); + + // Create a list of the selected input Txids, as indicated by the + // `InputSelector`. + let selected_txids: Vec<_> = presigned + .utxo_inputs + .iter() + .map(|utxo| utxo.txid.clone()) + .collect(); + + // Create the list of selected inputs and update the COMMIT transaction. + let selected_inputs: Vec<_> = proto + .inputs + .into_iter() + .filter(|input| selected_txids.contains(&input.txid)) + .map(super::utils::hard_clone_proto_input) + .collect::>()?; + + commit_signing.inputs = selected_inputs; + + // Update the change amount to calculated amount. + if !proto.disable_change_output && presigned.utxo_outputs.len() == 2 { + let change_amount = presigned + .utxo_outputs + .last() + .expect("No Utxo outputs generated") + .value; + + let mut change_output = change_output.expect("change output expected"); + change_output.value = change_amount; + + commit_signing + .outputs + .push(hard_clone_proto_output(change_output)?); + } + + commit_signing.input_selector = UtxoProto::InputSelector::UseAll; + commit_signing.disable_change_output = true; + commit_signing.fee_per_vb = 0; + commit_signing.change_output = Default::default(); + + // Now we construct the *actual* REVEAL transaction. + + let brc20_input = Proto::Input { + value: brc20_output_value, + txid: commit_txid.into(), // Reference COMMIT transaction. + sighash_type: UtxoProto::SighashType::UseDefault, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::brc20_inscribe(brc20_info.clone()), + }), + ..Default::default() + }; + + // Build the REVEAL transaction. + let reveal_signing = Proto::SigningInput { + private_key: proto.private_key.to_vec().into(), + inputs: vec![brc20_input], + outputs: vec![tagged_output], + input_selector: UtxoProto::InputSelector::UseAll, + change_output: Default::default(), + fee_per_vb: 0, + disable_change_output: true, + ..Default::default() + }; + + let reveal_signing = pre_processor(reveal_signing); + + Ok(Proto::mod_TransactionPlan::Brc20Plan { + commit: Some(commit_signing), + reveal: Some(reveal_signing), + }) + } +} diff --git a/rust/tw_bitcoin/src/modules/signer.rs b/rust/tw_bitcoin/src/modules/signer.rs new file mode 100644 index 00000000000..cdf17b68eef --- /dev/null +++ b/rust/tw_bitcoin/src/modules/signer.rs @@ -0,0 +1,159 @@ +use crate::{BitcoinEntry, Error, Result}; +use bitcoin::key::{TapTweak, TweakedKeyPair}; +use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; +use secp256k1::{KeyPair, Message, Secp256k1}; +use std::collections::HashMap; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PrivateKeyBytes, SignatureBytes}; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +pub struct Signer; + +impl Signer { + pub fn sign_proto( + _coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + ) -> Result> { + // Technically not required here, since this gets called by + // `preimage_hashes_impl` and `compile_impl`. But we're leaving this + // here in case this methods gets extended and the pre-processing does + // not get accidentally forgotten. + let proto = crate::entry::pre_processor(proto); + + // Collect individual private keys per input, if there are any. + let mut individual_keys = HashMap::new(); + for (index, txin) in proto.inputs.iter().enumerate() { + if !txin.private_key.is_empty() { + individual_keys.insert(index, txin.private_key.to_vec()); + } + } + + // Generate the sighashes. + let pre_signed = BitcoinEntry.preimage_hashes_impl(_coin, proto.clone())?; + + // Check for error. + if pre_signed.error != Proto::Error::OK { + return Err(Error::from(pre_signed.error)); + } + + // Sign the sighashes. + let signatures = crate::modules::signer::Signer::signatures_from_proto( + &pre_signed, + proto.private_key.to_vec(), + individual_keys, + proto.dangerous_use_fixed_schnorr_rng, + )?; + + // Construct the final transaction. + BitcoinEntry.compile_impl(_coin, proto, signatures, vec![]) + } + pub fn signatures_from_proto( + input: &Proto::PreSigningOutput<'_>, + private_key: PrivateKeyBytes, + individual_keys: HashMap, + dangerous_use_fixed_schnorr_rng: bool, + ) -> Result> { + let secp = Secp256k1::new(); + + let mut signatures = vec![]; + + for (index, (entry, utxo)) in input + .sighashes + .iter() + .zip(input.utxo_inputs.iter()) + .enumerate() + { + // Check if there's an individual private key for the given input. If not, use the primary one. + let keypair = if let Some(slice) = individual_keys.get(&index) { + KeyPair::from_seckey_slice(&secp, slice) + .map_err(|_| Error::from(Proto::Error::Error_invalid_private_key))? + } else { + KeyPair::from_seckey_slice(&secp, private_key.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_private_key))? + }; + + // Create signable message from sighash. + let sighash = Message::from_slice(entry.sighash.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_sighash))?; + + // Sign the sighash depending on signing method. + match entry.signing_method { + // Create a ECDSA signature for legacy and segwit transaction. + UtxoProto::SigningMethod::Legacy | UtxoProto::SigningMethod::Segwit => { + let sighash_type = + if let UtxoProto::SighashType::UseDefault = entry.sighash_type { + EcdsaSighashType::All + } else { + EcdsaSighashType::from_consensus(entry.sighash_type as u32) + }; + + let sig = bitcoin::ecdsa::Signature { + sig: keypair.secret_key().sign_ecdsa(sighash), + hash_ty: sighash_type, + }; + + signatures.push(sig.serialize().to_vec()); + }, + // Create a Schnorr signature for taproot transactions. + UtxoProto::SigningMethod::TaprootAll + | UtxoProto::SigningMethod::TaprootOnePrevout => { + // Note that `input.sighash_type = 0` is handled by the underlying library. + let sighash_type = TapSighashType::from_consensus_u8(entry.sighash_type as u8) + .map_err(|_| Error::from(Proto::Error::Error_utxo_invalid_sighash_type))?; + + // Any empty leaf hash implies P2TR key-path (balance transfer) + if utxo.leaf_hash.is_empty() { + // Tweak keypair for P2TR key-path (ie. zeroed Merkle root). + let tapped: TweakedKeyPair = keypair.tap_tweak(&secp, None); + let tweaked = KeyPair::from(tapped); + + // Construct the Schnorr signature. + let schnorr = if dangerous_use_fixed_schnorr_rng { + // For tests, we disable the included randomness in order to create + // reproducible signatures. Randomness should ALWAYS be used in + // production. + secp.sign_schnorr_no_aux_rand(&sighash, &tweaked) + } else { + secp.sign_schnorr(&sighash, &tweaked) + }; + + let sig = bitcoin::taproot::Signature { + sig: schnorr, + hash_ty: sighash_type, + }; + + signatures.push(sig.to_vec()); + } + // If it has a leaf hash, then it's a P2TR script-path (complex transaction) + else { + // NOTE: We do not tweak the key here since the complex + // spending condition(s) must take into account on who + // is allowed to spend the input, hence this signing + // process is simpler than for P2TR key-path. + + // Construct the Schnorr signature. + let schnorr = if dangerous_use_fixed_schnorr_rng { + // For tests, we disable the included randomness in order to create + // reproducible signatures. Randomness should ALWAYS be used in + // production. + secp.sign_schnorr_no_aux_rand(&sighash, &keypair) + } else { + secp.sign_schnorr(&sighash, &keypair) + }; + + let sig = bitcoin::taproot::Signature { + sig: schnorr, + hash_ty: sighash_type, + }; + + signatures.push(sig.to_vec()); + } + }, + } + } + + Ok(signatures) + } +} diff --git a/rust/tw_bitcoin/src/modules/transactions/brc20.rs b/rust/tw_bitcoin/src/modules/transactions/brc20.rs new file mode 100644 index 00000000000..51afda68777 --- /dev/null +++ b/rust/tw_bitcoin/src/modules/transactions/brc20.rs @@ -0,0 +1,93 @@ +use super::ordinals::OrdinalsInscription; +use crate::{Error, Result}; +use bitcoin::PublicKey; +use serde::Serialize; +use tw_proto::BitcoinV2::Proto; + +#[derive(Debug, Clone, Serialize)] +pub struct Brc20Ticker(String); + +impl Brc20Ticker { + pub fn new(string: String) -> Result { + // Brc20Ticker must be a 4-letter identifier. + if string.len() != 4 { + return Err(Error::from(Proto::Error::Error_invalid_brc20_ticker)); + } + + Ok(Brc20Ticker(string)) + } +} + +#[derive(Serialize)] +struct BRC20TransferPayload { + #[serde(rename = "p")] + protocol: String, + #[serde(rename = "op")] + operation: String, + #[serde(rename = "tick")] + ticker: Brc20Ticker, + #[serde(rename = "amt")] + amount: String, +} + +impl BRC20TransferPayload { + const PROTOCOL_ID: &str = "brc-20"; + const MIME: &[u8] = b"text/plain;charset=utf-8"; +} + +impl BRC20TransferPayload { + const OPERATION: &str = "transfer"; + + fn new(ticker: Brc20Ticker, value: u64) -> Self { + BRC20TransferPayload { + protocol: Self::PROTOCOL_ID.to_string(), + operation: Self::OPERATION.to_string(), + ticker, + amount: value.to_string(), + } + } +} + +pub struct BRC20TransferInscription(OrdinalsInscription); + +impl BRC20TransferInscription { + pub fn new( + recipient: PublicKey, + ticker: Brc20Ticker, + value: u64, + ) -> Result { + let data: BRC20TransferPayload = BRC20TransferPayload::new(ticker, value); + + let inscription = OrdinalsInscription::new( + BRC20TransferPayload::MIME, + &serde_json::to_vec(&data).expect("badly constructed BRC20 payload"), + recipient, + )?; + + Ok(BRC20TransferInscription(inscription)) + } + pub fn inscription(&self) -> &OrdinalsInscription { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn brc20_ticker_validity() { + // Must be four characters. + let ticker = Brc20Ticker::new("invalid".to_string()); + assert!(ticker.is_err()); + + let ticker = Brc20Ticker::new("asdf".to_string()); + assert!(ticker.is_ok()); + + // Cover clone implemenation. + let ticker = ticker.unwrap(); + + let _cloned = ticker.clone(); + let _ticker = ticker; + } +} diff --git a/rust/tw_bitcoin/src/modules/transactions/input_builder.rs b/rust/tw_bitcoin/src/modules/transactions/input_builder.rs new file mode 100644 index 00000000000..4d1660cb61f --- /dev/null +++ b/rust/tw_bitcoin/src/modules/transactions/input_builder.rs @@ -0,0 +1,320 @@ +use super::brc20::{BRC20TransferInscription, Brc20Ticker}; +use crate::aliases::*; +use crate::modules::transactions::OrdinalNftInscription; +use crate::{Error, Result}; +use bitcoin::taproot::{LeafVersion, TapLeafHash}; +use bitcoin::ScriptBuf; +use secp256k1::XOnlyPublicKey; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +// Convenience varibles used solely for readability. +const NO_LEAF_HASH: Option = None; + +pub struct InputBuilder; + +impl InputBuilder { + pub fn utxo_from_proto(input: &Proto::Input<'_>) -> Result> { + let (signing_method, script_pubkey, leaf_hash, weight) = match &input.to_recipient { + ProtoInputRecipient::builder(builder) => match &builder.variant { + ProtoInputBuilder::p2sh(redeem_script) => { + // The scriptPubkey is the redeem script directly. + let script_pubkey = ScriptBuf::from_bytes(redeem_script.to_vec()); + + ( + UtxoProto::SigningMethod::Legacy, + script_pubkey, + NO_LEAF_HASH, + // scale factor applied to non-witness bytes + 4 * ( + // length + redeem script. + 1 + redeem_script.len() as u64 + ), + ) + }, + ProtoInputBuilder::p2pkh(pubkey) => { + let pubkey = bitcoin::PublicKey::from_slice(pubkey.as_ref())?; + let script_pubkey = ScriptBuf::new_p2pkh(&pubkey.pubkey_hash()); + + ( + UtxoProto::SigningMethod::Legacy, + script_pubkey, + NO_LEAF_HASH, + // scale factor applied to non-witness bytes + 4 * ( + // length + ECDSA signature + 1 + 72 + + // length + public key + 1 + { + if pubkey.compressed { + 33 + } else { + 65 + } + } + ), + ) + }, + ProtoInputBuilder::p2wsh(redeem_script) => { + // The scriptPubkey is the redeem script directly. + let script_pubkey = ScriptBuf::from_bytes(redeem_script.to_vec()); + + ( + UtxoProto::SigningMethod::Segwit, + script_pubkey, + NO_LEAF_HASH, + // witness bytes, scale factor NOT applied. + ( + // length + redeem script. + 1 + redeem_script.len() as u64 + ), + ) + }, + ProtoInputBuilder::p2wpkh(pubkey) => { + let pubkey = bitcoin::PublicKey::from_slice(pubkey.as_ref())?; + + let script_pubkey = + ScriptBuf::new_v0_p2wpkh(&pubkey.wpubkey_hash().ok_or_else(|| { + Error::from(Proto::Error::Error_invalid_witness_pubkey_hash) + })?) + // Special script code requirement for claiming P2WPKH outputs. + .p2wpkh_script_code() + .ok_or_else(|| Error::from(Proto::Error::Error_invalid_wpkh_script_code))?; + + ( + UtxoProto::SigningMethod::Segwit, + script_pubkey, + NO_LEAF_HASH, + // witness bytes, scale factor NOT applied. + ( + // indicator of witness item (2) + 1 + + // length + ECDSA signature (can be 71 or 72) + 1 + 72 + + // length + public key + 1 + { + if pubkey.compressed { + 33 + } else { + 65 + } + } + ), + ) + }, + ProtoInputBuilder::p2tr_key_path(key_path) => { + let pubkey = bitcoin::PublicKey::from_slice(key_path.public_key.as_ref())?; + let xonly = XOnlyPublicKey::from(pubkey.inner); + + let signing_method = if key_path.one_prevout { + UtxoProto::SigningMethod::TaprootOnePrevout + } else { + UtxoProto::SigningMethod::TaprootAll + }; + + let script_pubkey = + ScriptBuf::new_v1_p2tr(&secp256k1::Secp256k1::new(), xonly, None); + + ( + signing_method, + script_pubkey, + NO_LEAF_HASH, + // witness bytes, scale factor NOT applied. + ( + // indicator of witness item (1) + 1 + + // length + Schnorr signature (can be 71 or 72) + 1 + 72 + // NO public key + ), + ) + }, + ProtoInputBuilder::p2tr_script_path(complex) => { + let script_pubkey = ScriptBuf::from_bytes(complex.payload.to_vec()); + let leaf_hash = Some(TapLeafHash::from_script( + script_pubkey.as_script(), + bitcoin::taproot::LeafVersion::TapScript, + )); + + let signing_method = if complex.one_prevout { + UtxoProto::SigningMethod::TaprootOnePrevout + } else { + UtxoProto::SigningMethod::TaprootAll + }; + + ( + signing_method, + script_pubkey, + leaf_hash, + // witness bytes, scale factor NOT applied. + ( + // indicator of witness item + 1 + + // the payload/witness + complex.payload.len() as u64 + ), + ) + }, + ProtoInputBuilder::ordinal_inscribe(ordinal) => { + let pubkey = bitcoin::PublicKey::from_slice(ordinal.inscribe_to.as_ref())?; + let mime_type = ordinal.mime_type.as_ref(); + let data = ordinal.payload.as_ref(); + + let nft = OrdinalNftInscription::new(mime_type.as_bytes(), data, pubkey) + .expect("badly constructed Ordinal inscription"); + + // We construct a control block to estimate the fee, + // otherwise we do not need it here. + let control_block = nft + .inscription() + .spend_info() + .control_block(&( + nft.inscription().taproot_program().to_owned(), + LeafVersion::TapScript, + )) + .expect("badly constructed control block"); + + let leaf_hash = Some(TapLeafHash::from_script( + nft.inscription().taproot_program(), + bitcoin::taproot::LeafVersion::TapScript, + )); + + let signing_method = if ordinal.one_prevout { + UtxoProto::SigningMethod::TaprootOnePrevout + } else { + UtxoProto::SigningMethod::TaprootAll + }; + + let script_pubkey = ScriptBuf::from(nft.inscription().taproot_program()); + + ( + signing_method, + // TODO: This is technically not needed here, since + // Sighash only includes the leaf hash, not the actual + // payload. Remove this (same for other complex scripts). + script_pubkey, + leaf_hash, + // witness bytes, scale factor NOT applied. + ( + // indicator of witness item (1) + 1 + + // length + Schnorr signature (can be 71 or 72) + 1 + 72 + + // the payload/witness + nft.inscription().taproot_program().len() as u64 + + // length + control block + 1 + control_block.size() as u64 + ), + ) + }, + // TODO: Unify this and `ordinal_inscribe` somehow + ProtoInputBuilder::brc20_inscribe(brc20) => { + let pubkey = bitcoin::PublicKey::from_slice(brc20.inscribe_to.as_ref())?; + let ticker = Brc20Ticker::new(brc20.ticker.to_string())?; + + let transfer = + BRC20TransferInscription::new(pubkey, ticker, brc20.transfer_amount) + .expect("invalid BRC20 transfer construction"); + + // We construct a control block to estimate the fee, + // otherwise we do not need it here. + let control_block = transfer + .inscription() + .spend_info() + .control_block(&( + transfer.inscription().taproot_program().to_owned(), + LeafVersion::TapScript, + )) + .expect("badly constructed control block"); + + let leaf_hash = Some(TapLeafHash::from_script( + transfer.inscription().taproot_program(), + bitcoin::taproot::LeafVersion::TapScript, + )); + + let signing_method = if brc20.one_prevout { + UtxoProto::SigningMethod::TaprootOnePrevout + } else { + UtxoProto::SigningMethod::TaprootAll + }; + + let script_pubkey = ScriptBuf::from(transfer.inscription().taproot_program()); + + ( + signing_method, + script_pubkey, + leaf_hash, + // witness bytes, scale factor NOT applied. + ( + // indicator of witness item (1) + 1 + + // length + Schnorr signature (can be 71 or 72) + 1 + 72 + + // the payload/witness + transfer.inscription().taproot_program().len() as u64 + + // length + control block + 1 + control_block.size() as u64 + ), + ) + }, + ProtoInputBuilder::None => { + return Err(Error::from(Proto::Error::Error_missing_input_builder)) + }, + }, + ProtoInputRecipient::custom_script(custom) => { + let script_pubkey = ScriptBuf::from_bytes(custom.script_pubkey.to_vec()); + + #[rustfmt::skip] + let leaf_hash = if let UtxoProto::SigningMethod::TaprootAll + | UtxoProto::SigningMethod::TaprootOnePrevout = custom.signing_method + { + Some(TapLeafHash::from_script( + &script_pubkey, + bitcoin::taproot::LeafVersion::TapScript, + )) + } else { + None + }; + + ( + custom.signing_method, + script_pubkey, + leaf_hash, + ( + // scale factor applied to non-witness bytes + 4 * custom.script_sig.len() as u64 + // indicator of witness item count. + + 1 + // witness bytes, scale factor NOT applied. + + custom + .witness_items + .iter() + .map(|item| item.len() as u64) + .sum::() + ), + ) + }, + ProtoInputRecipient::None => { + return Err(Error::from(Proto::Error::Error_missing_input_builder)) + }, + }; + + // Create Utxo.proto structure. + let utxo = UtxoProto::TxIn { + txid: input.txid.to_vec().into(), + vout: input.vout, + value: input.value, + sequence: input.sequence, + script_pubkey: script_pubkey.to_vec().into(), + signing_method, + sighash_type: input.sighash_type, + weight_estimate: weight, + leaf_hash: leaf_hash + .map(|hash| hash.to_vec().into()) + .unwrap_or_default(), + }; + + Ok(utxo) + } +} diff --git a/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs b/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs new file mode 100644 index 00000000000..b93269031fc --- /dev/null +++ b/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs @@ -0,0 +1,172 @@ +use super::brc20::{BRC20TransferInscription, Brc20Ticker}; +use super::OrdinalNftInscription; +use crate::aliases::*; +use crate::{Error, Result}; +use bitcoin::consensus::Decodable; +use bitcoin::taproot::{ControlBlock, LeafVersion}; +use bitcoin::{ScriptBuf, Witness}; +use std::borrow::Cow; +use tw_coin_entry::coin_entry::SignatureBytes; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +pub struct InputClaimBuilder; + +impl InputClaimBuilder { + /// Creates the claim script (_scriptSig_ or _Witness_) to be revealed + /// on-chain for a given input. + pub fn utxo_claim_from_proto( + input: &Proto::Input<'_>, + signature: SignatureBytes, + ) -> Result> { + let (script_sig, witness) = match &input.to_recipient { + ProtoInputRecipient::builder(variant) => match &variant.variant { + ProtoInputBuilder::p2sh(redeem_script) => ( + ScriptBuf::from_bytes(redeem_script.to_vec()), + Witness::new(), + ), + ProtoInputBuilder::p2pkh(pubkey) => { + let sig = bitcoin::ecdsa::Signature::from_slice(signature.as_ref())?; + let pubkey = bitcoin::PublicKey::from_slice(pubkey.as_ref())?; + + // The spending script itself. + ( + ScriptBuf::builder() + .push_slice(sig.serialize()) + .push_key(&pubkey) + .into_script(), + Witness::new(), + ) + }, + ProtoInputBuilder::p2wsh(redeem_script) => { + let witness = Witness::consensus_decode(&mut redeem_script.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_witness_encoding))?; + + (ScriptBuf::new(), witness) + }, + ProtoInputBuilder::p2wpkh(pubkey) => { + let sig = bitcoin::ecdsa::Signature::from_slice(signature.as_ref())?; + let pubkey = bitcoin::PublicKey::from_slice(pubkey.as_ref())?; + + // The spending script itself. + (ScriptBuf::new(), { + let mut w = Witness::new(); + w.push(sig.serialize()); + w.push(pubkey.to_bytes()); + w + }) + }, + ProtoInputBuilder::p2tr_key_path(_) => { + let sig = bitcoin::taproot::Signature::from_slice(signature.as_ref())?; + + // The spending script itself. + (ScriptBuf::new(), { + let mut w = Witness::new(); + w.push(sig.to_vec()); + w + }) + }, + ProtoInputBuilder::p2tr_script_path(taproot) => { + let control_block = ControlBlock::decode(taproot.control_block.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_control_block))?; + + let sig = bitcoin::taproot::Signature::from_slice(signature.as_ref())?; + + // The spending script itself. + (ScriptBuf::new(), { + let mut w = Witness::new(); + w.push(sig.to_vec()); + w.push(taproot.payload.as_ref()); + w.push(control_block.serialize()); + w + }) + }, + ProtoInputBuilder::ordinal_inscribe(ordinal) => { + let pubkey = bitcoin::PublicKey::from_slice(ordinal.inscribe_to.as_ref())?; + let mime_type = ordinal.mime_type.as_ref(); + let data = ordinal.payload.as_ref(); + + let nft = OrdinalNftInscription::new(mime_type.as_bytes(), data, pubkey) + .expect("badly constructed Ordinal inscription"); + + // Create a control block for that inscription. + let control_block = nft + .inscription() + .spend_info() + .control_block(&( + nft.inscription().taproot_program().to_owned(), + LeafVersion::TapScript, + )) + .expect("badly constructed control block"); + + let sig = bitcoin::taproot::Signature::from_slice(signature.as_ref())?; + + // The spending script itself. + (ScriptBuf::new(), { + let mut w = Witness::new(); + w.push(sig.to_vec()); + w.push(nft.inscription().taproot_program()); + w.push(control_block.serialize()); + w + }) + }, + ProtoInputBuilder::brc20_inscribe(brc20) => { + let pubkey = bitcoin::PublicKey::from_slice(brc20.inscribe_to.as_ref())?; + let ticker = Brc20Ticker::new(brc20.ticker.to_string())?; + + // Construct the BRC20 transfer inscription. + let transfer = + BRC20TransferInscription::new(pubkey, ticker, brc20.transfer_amount) + .expect("invalid BRC20 transfer construction"); + + // Create a control block for that inscription. + let control_block = transfer + .inscription() + .spend_info() + .control_block(&( + transfer.inscription().taproot_program().to_owned(), + LeafVersion::TapScript, + )) + .expect("badly constructed control block"); + + let sig = bitcoin::taproot::Signature::from_slice(signature.as_ref())?; + + // The spending script itself. + (ScriptBuf::new(), { + let mut w = Witness::new(); + w.push(sig.to_vec()); + w.push(transfer.inscription().taproot_program()); + w.push(control_block.serialize()); + w + }) + }, + ProtoInputBuilder::None => { + return Err(Error::from(Proto::Error::Error_missing_input_builder)) + }, + }, + ProtoInputRecipient::custom_script(custom) => ( + ScriptBuf::from_bytes(custom.script_sig.to_vec()), + Witness::from_slice(&custom.witness_items), + ), + ProtoInputRecipient::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + // Create Utxo.proto structure. + let claim = UtxoProto::TxInClaim { + txid: input.txid.to_vec().into(), + vout: input.vout, + sequence: input.sequence, + script_sig: script_sig.to_vec().into(), + witness_items: witness + .to_vec() + .into_iter() + .map(Cow::Owned) + .collect::>>(), + }; + + Ok(claim) + } +} diff --git a/rust/tw_bitcoin/src/modules/transactions/mod.rs b/rust/tw_bitcoin/src/modules/transactions/mod.rs new file mode 100644 index 00000000000..03172284046 --- /dev/null +++ b/rust/tw_bitcoin/src/modules/transactions/mod.rs @@ -0,0 +1,26 @@ +use bitcoin::key::PublicKey; +use bitcoin::script::ScriptBuf; +use bitcoin::taproot::{TapNodeHash, TaprootSpendInfo}; + +mod brc20; +mod input_builder; +mod input_claim_builder; +mod ordinals; +mod output_builder; + +// Re-exports +pub use brc20::{BRC20TransferInscription, Brc20Ticker}; +pub use input_builder::InputBuilder; +pub use input_claim_builder::InputClaimBuilder; +pub use ordinals::{OrdinalNftInscription, OrdinalsInscription}; +pub use output_builder::OutputBuilder; + +pub struct TaprootScript { + pub pubkey: PublicKey, + pub merkle_root: TapNodeHash, +} + +pub struct TaprootProgram { + pub script: ScriptBuf, + pub spend_info: TaprootSpendInfo, +} diff --git a/rust/tw_bitcoin/src/ordinals.rs b/rust/tw_bitcoin/src/modules/transactions/ordinals.rs similarity index 74% rename from rust/tw_bitcoin/src/ordinals.rs rename to rust/tw_bitcoin/src/modules/transactions/ordinals.rs index 35c48a02a0c..76b4a221631 100644 --- a/rust/tw_bitcoin/src/ordinals.rs +++ b/rust/tw_bitcoin/src/modules/transactions/ordinals.rs @@ -1,35 +1,22 @@ -use crate::{Error, Recipient, Result, TaprootProgram, TaprootScript}; +use super::TaprootProgram; +use crate::{Error, Result}; use bitcoin::script::{PushBytesBuf, ScriptBuf}; use bitcoin::secp256k1::XOnlyPublicKey; use bitcoin::taproot::{TaprootBuilder, TaprootSpendInfo}; use bitcoin::{PublicKey, Script}; +use tw_proto::BitcoinV2::Proto; -#[derive(Debug, Clone)] pub struct OrdinalsInscription { envelope: TaprootProgram, - recipient: Recipient, } impl OrdinalsInscription { /// Creates a new Ordinals Inscription ("commit stage"). - pub fn new( - mime: &[u8], - data: &[u8], - recipient: Recipient, - ) -> Result { + pub fn new(mime: &[u8], data: &[u8], recipient: PublicKey) -> Result { // Create the envelope, containing the inscription content. - let envelope = create_envelope(mime, data, recipient.public_key())?; + let envelope = create_envelope(mime, data, recipient)?; - // Compute the merkle root of the inscription. - let merkle_root = envelope - .spend_info - .merkle_root() - .expect("Ordinals envelope not constructed correctly"); - - Ok(OrdinalsInscription { - envelope, - recipient: Recipient::::from_pubkey_recipient(recipient, merkle_root), - }) + Ok(OrdinalsInscription { envelope }) } pub fn taproot_program(&self) -> &Script { self.envelope.script.as_script() @@ -37,9 +24,6 @@ impl OrdinalsInscription { pub fn spend_info(&self) -> &TaprootSpendInfo { &self.envelope.spend_info } - pub fn recipient(&self) -> &Recipient { - &self.recipient - } } /// Creates an [Ordinals Inscription](https://docs.ordinals.com/inscriptions.html). @@ -63,7 +47,9 @@ fn create_envelope(mime: &[u8], data: &[u8], internal_key: PublicKey) -> Result< // Create MIME buffer. let mut mime_buf = PushBytesBuf::new(); - mime_buf.extend_from_slice(mime).map_err(|_| Error::Todo)?; + mime_buf + .extend_from_slice(mime) + .map_err(|_| Error::from(Proto::Error::Error_ordinal_mime_type_too_large))?; // Create an Ordinals Inscription. let mut builder = ScriptBuf::builder() @@ -89,7 +75,9 @@ fn create_envelope(mime: &[u8], data: &[u8], internal_key: PublicKey) -> Result< for chunk in data.chunks(520) { // Create data buffer. let mut data_buf = PushBytesBuf::new(); - data_buf.extend_from_slice(chunk).map_err(|_| Error::Todo)?; + data_buf + .extend_from_slice(chunk) + .map_err(|_| Error::from(Proto::Error::Error_ordinal_payload_too_large))?; // Push buffer builder = builder.push_slice(data_buf); @@ -113,3 +101,25 @@ fn create_envelope(mime: &[u8], data: &[u8], internal_key: PublicKey) -> Result< Ok(TaprootProgram { script, spend_info }) } + +pub struct OrdinalNftInscription(OrdinalsInscription); + +impl OrdinalNftInscription { + // Constructs an [Ordinal inscription] with a given MIME type. Common MIME + // types are: + // * "application/json", + // * "application/pdf", + // * "image/gif", + // * "image/jpeg", + // * "image/png", + // * "text/plain;charset=utf-8" + // * ... + // + // [Ordinal inscription]: https://docs.ordinals.com/inscriptions.html + pub fn new(mime_type: &[u8], data: &[u8], recipient: PublicKey) -> Result { + OrdinalsInscription::new(mime_type, data, recipient).map(OrdinalNftInscription) + } + pub fn inscription(&self) -> &OrdinalsInscription { + &self.0 + } +} diff --git a/rust/tw_bitcoin/src/modules/transactions/output_builder.rs b/rust/tw_bitcoin/src/modules/transactions/output_builder.rs new file mode 100644 index 00000000000..72fef4c0c39 --- /dev/null +++ b/rust/tw_bitcoin/src/modules/transactions/output_builder.rs @@ -0,0 +1,371 @@ +use std::str::FromStr; + +use super::brc20::{BRC20TransferInscription, Brc20Ticker}; +use super::OrdinalNftInscription; +use crate::aliases::*; +use crate::{Error, Result}; +use bitcoin::address::{Payload, WitnessVersion}; +use bitcoin::key::TweakedPublicKey; +use bitcoin::taproot::{LeafVersion, TapNodeHash}; +use bitcoin::{Address, PubkeyHash, ScriptBuf, ScriptHash, WPubkeyHash, WScriptHash}; +use secp256k1::hashes::Hash; +use secp256k1::XOnlyPublicKey; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; + +pub struct OutputBuilder; + +// Convenience varibles used solely for readability. +const NO_CONTROL_BLOCK: Option> = None; +const NO_TAPROOT_PAYLOAD: Option> = None; + +impl OutputBuilder { + /// Creates the spending condition (_scriptPubkey_) for a given output. + pub fn utxo_from_proto( + output: &Proto::Output<'_>, + ) -> Result> { + let secp = secp256k1::Secp256k1::new(); + + let (script_pubkey, control_block, taproot_payload) = match &output.to_recipient { + // Script spending condition was passed on directly. + ProtoOutputRecipient::custom_script_pubkey(script) => ( + ScriptBuf::from_bytes(script.to_vec()), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ), + // Process builder methods. We construct the Script spending + // conditions by using the specified parameters. + ProtoOutputRecipient::builder(builder) => match &builder.variant { + ProtoOutputBuilder::p2sh(script_or_hash) => { + let script_hash = redeem_script_or_hash(script_or_hash)?; + ( + ScriptBuf::new_p2sh(&script_hash), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::p2pkh(pubkey_or_hash) => { + let pubkey_hash = pubkey_hash_from_proto(pubkey_or_hash)?; + ( + ScriptBuf::new_p2pkh(&pubkey_hash), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::p2wsh(script_or_hash) => { + let wscript_hash = witness_redeem_script_or_hash(script_or_hash)?; + ( + ScriptBuf::new_v0_p2wsh(&wscript_hash), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::p2wpkh(pubkey_or_hash) => { + let wpubkey_hash = witness_pubkey_hash_from_proto(pubkey_or_hash)?; + ( + ScriptBuf::new_v0_p2wpkh(&wpubkey_hash), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::p2tr_key_path(pubkey) => { + let pubkey = bitcoin::PublicKey::from_slice(pubkey.as_ref())?; + let xonly = XOnlyPublicKey::from(pubkey.inner); + ( + ScriptBuf::new_v1_p2tr(&secp, xonly, None), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::p2tr_script_path(complex) => { + let node_hash = TapNodeHash::from_slice(complex.merkle_root.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_taproot_root))?; + + let pubkey = bitcoin::PublicKey::from_slice(complex.internal_key.as_ref())?; + let xonly = XOnlyPublicKey::from(pubkey.inner); + + ( + ScriptBuf::new_v1_p2tr(&secp, xonly, Some(node_hash)), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::p2tr_dangerous_assume_tweaked(tweaked_pubkey) => { + let xonly = XOnlyPublicKey::from_slice(tweaked_pubkey).map_err(|_| { + Error::from(Proto::Error::Error_invalid_taproot_tweaked_pubkey) + })?; + + let tweaked = TweakedPublicKey::dangerous_assume_tweaked(xonly); + + ( + ScriptBuf::new_v1_p2tr_tweaked(tweaked), + NO_CONTROL_BLOCK, + NO_TAPROOT_PAYLOAD, + ) + }, + ProtoOutputBuilder::ordinal_inscribe(ordinal) => { + let pubkey = bitcoin::PublicKey::from_slice(ordinal.inscribe_to.as_ref())?; + let xonly = XOnlyPublicKey::from(pubkey.inner); + let mime_type = ordinal.mime_type.as_ref(); + let data = ordinal.payload.as_ref(); + + let nft = OrdinalNftInscription::new(mime_type.as_bytes(), data, pubkey) + .expect("badly constructed Ordinal inscription"); + + // Construct the control block. + let control_block = nft + .inscription() + .spend_info() + .control_block(&( + nft.inscription().taproot_program().to_owned(), + LeafVersion::TapScript, + )) + .expect("badly constructed control block"); + + // Construct the merkle root. + let merkle_root = nft + .inscription() + .spend_info() + .merkle_root() + .expect("badly constructed Taproot merkle root"); + + ( + ScriptBuf::new_v1_p2tr(&secp, xonly, Some(merkle_root)), + Some(control_block.serialize()), + Some(nft.inscription().taproot_program().to_vec()), + ) + }, + ProtoOutputBuilder::brc20_inscribe(brc20) => { + let pubkey = bitcoin::PublicKey::from_slice(brc20.inscribe_to.as_ref())?; + let xonly = XOnlyPublicKey::from(pubkey.inner); + + let ticker = Brc20Ticker::new(brc20.ticker.to_string())?; + let transfer = + BRC20TransferInscription::new(pubkey, ticker, brc20.transfer_amount) + .expect("invalid BRC20 transfer construction"); + + // Construct the control block. + let control_block = transfer + .inscription() + .spend_info() + .control_block(&( + transfer.inscription().taproot_program().to_owned(), + LeafVersion::TapScript, + )) + .expect("badly constructed control block"); + + // Construct the merkle root. + let merkle_root = transfer + .inscription() + .spend_info() + .merkle_root() + .expect("badly constructed Taproot merkle root"); + + ( + ScriptBuf::new_v1_p2tr(&secp, xonly, Some(merkle_root)), + Some(control_block.serialize()), + Some(transfer.inscription().taproot_program().to_vec()), + ) + }, + ProtoOutputBuilder::None => { + return Err(Error::from(Proto::Error::Error_missing_output_builder)) + }, + }, + // We derive the transaction type from the address. + ProtoOutputRecipient::from_address(addr) => { + let proto = output_from_address(output.value, addr.as_ref())?; + + // Recursive call, will initiate the appropraite builder. + return Self::utxo_from_proto(&proto); + }, + ProtoOutputRecipient::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + let utxo = Proto::mod_PreSigningOutput::TxOut { + value: output.value, + script_pubkey: script_pubkey.to_vec().into(), + control_block: control_block.map(|cb| cb.into()).unwrap_or_default(), + taproot_payload: taproot_payload.map(|cb| cb.into()).unwrap_or_default(), + }; + + Ok(utxo) + } +} + +// Convenience helper function. +fn redeem_script_or_hash( + script_or_hash: &Proto::mod_Output::OutputRedeemScriptOrHash, +) -> Result { + let pubkey_hash = match &script_or_hash.variant { + ProtoRedeemScriptOrHash::hash(hash) => ScriptHash::from_slice(hash.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_redeem_script))?, + ProtoRedeemScriptOrHash::redeem_script(script) => { + ScriptBuf::from_bytes(script.to_vec()).script_hash() + }, + ProtoRedeemScriptOrHash::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + Ok(pubkey_hash) +} + +// Convenience helper function. +fn witness_redeem_script_or_hash( + script_or_hash: &Proto::mod_Output::OutputRedeemScriptOrHash, +) -> Result { + let pubkey_hash = match &script_or_hash.variant { + ProtoRedeemScriptOrHash::hash(hash) => WScriptHash::from_slice(hash) + .map_err(|_| Error::from(Proto::Error::Error_invalid_witness_redeem_script_hash))?, + ProtoRedeemScriptOrHash::redeem_script(script) => { + ScriptBuf::from_bytes(script.to_vec()).wscript_hash() + }, + ProtoRedeemScriptOrHash::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + Ok(pubkey_hash) +} + +// Convenience helper function. +fn pubkey_hash_from_proto(pubkey_or_hash: &Proto::ToPublicKeyOrHash) -> Result { + let pubkey_hash = match &pubkey_or_hash.to_address { + ProtoPubkeyOrHash::hash(hash) => PubkeyHash::from_slice(hash.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_pubkey_hash))?, + ProtoPubkeyOrHash::pubkey(pubkey) => { + bitcoin::PublicKey::from_slice(pubkey.as_ref())?.pubkey_hash() + }, + ProtoPubkeyOrHash::None => return Err(Error::from(Proto::Error::Error_missing_recipient)), + }; + + Ok(pubkey_hash) +} + +// Convenience helper function. +fn witness_pubkey_hash_from_proto( + pubkey_or_hash: &Proto::ToPublicKeyOrHash, +) -> Result { + let wpubkey_hash = match &pubkey_or_hash.to_address { + ProtoPubkeyOrHash::hash(hash) => WPubkeyHash::from_slice(hash.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_witness_pubkey_hash))?, + ProtoPubkeyOrHash::pubkey(pubkey) => bitcoin::PublicKey::from_slice(pubkey.as_ref())? + .wpubkey_hash() + .ok_or_else(|| Error::from(Proto::Error::Error_invalid_witness_pubkey_hash))?, + ProtoPubkeyOrHash::None => return Err(Error::from(Proto::Error::Error_missing_recipient)), + }; + + Ok(wpubkey_hash) +} + +// Derives the P2* output from the given address. +fn output_from_address(value: u64, addr: &str) -> Result> { + let string = String::from_utf8(addr.to_vec()) + .map_err(|_| Error::from(Proto::Error::Error_bad_address_recipient))?; + + let addr = Address::from_str(&string) + .map_err(|_| Error::from(Proto::Error::Error_bad_address_recipient))? + .require_network(bitcoin::Network::Bitcoin) + .map_err(|_| Error::from(Proto::Error::Error_bad_address_recipient))?; + + let proto = match addr.payload { + // Identified a "PubkeyHash" address (i.e. P2PKH). + Payload::PubkeyHash(pubkey_hash) => Proto::Output { + value, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::hash(pubkey_hash.to_vec().into()), + }), + }), + }, + // Identified a witness program (i.e. Segwit or Taproot). + Payload::WitnessProgram(progam) => { + match progam.version() { + // Identified version 0, i.e. Segwit + WitnessVersion::V0 => { + let payload = progam.program().as_bytes().to_vec(); + + // Check for P2WPKH. + if payload.len() == 20 { + return Ok(Proto::Output { + value, + to_recipient: ProtoOutputRecipient::builder( + Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::hash( + // Payload is the WPubkey hash. + payload.into(), + ), + }), + }, + ), + }); + } + + // Check for P2WSH. + if payload.len() == 32 { + return Ok(Proto::Output { + value, + to_recipient: ProtoOutputRecipient::builder( + Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wsh( + Proto::mod_Output::OutputRedeemScriptOrHash { + variant: ProtoOutputRedeemScriptOrHashBuilder::hash( + // Payload is the WScript hash. + payload.to_vec().into(), + ), + }, + ), + }, + ), + }); + } + + return Err(Error::from(Proto::Error::Error_bad_address_recipient)); + }, + // Identified version 1, i.e P2TR key-path (Taproot) + WitnessVersion::V1 => { + let pubkey = progam.program().as_bytes().to_vec(); + if pubkey.len() != 32 { + return Err(Error::from(Proto::Error::Error_bad_address_recipient)); + } + + Proto::Output { + value, + to_recipient: ProtoOutputRecipient::builder( + Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2tr_dangerous_assume_tweaked( + pubkey.into(), + ), + }, + ), + } + }, + _ => { + return Err(Error::from( + Proto::Error::Error_unsupported_address_recipient, + )) + }, + } + }, + Payload::ScriptHash(script_hash) => Proto::Output { + value, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2sh(Proto::mod_Output::OutputRedeemScriptOrHash { + variant: ProtoOutputRedeemScriptOrHashBuilder::hash( + script_hash.to_vec().into(), + ), + }), + }), + }, + _ => { + return Err(Error::from( + Proto::Error::Error_unsupported_address_recipient, + )) + }, + }; + + Ok(proto) +} diff --git a/rust/tw_bitcoin/src/modules/utils.rs b/rust/tw_bitcoin/src/modules/utils.rs new file mode 100644 index 00000000000..f64f9bfb29e --- /dev/null +++ b/rust/tw_bitcoin/src/modules/utils.rs @@ -0,0 +1,188 @@ +use crate::aliases::*; +use crate::{Error, Result}; +use tw_proto::BitcoinV2::Proto; + +// Convenience function: our protobuf library wraps certain types (such as +// `bytes`) in `Cow`, but given that calling `clone()` on a `Cow::Borrowed(T)` +// does not actually clone the underlying data (but the smart pointer instead), +// we must hard-clone individual fields manually. This is unfortunately required +// due to how protobuf library works and our use of the 'static constraints. +pub fn hard_clone_proto_input(proto: Proto::Input<'_>) -> Result> { + fn new_builder(variant: ProtoInputBuilder<'static>) -> ProtoInputRecipient<'static> { + ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { variant }) + } + + let to_recipient = match proto.to_recipient { + ProtoInputRecipient::builder(builder) => match builder.variant { + ProtoInputBuilder::p2sh(script) => { + new_builder(ProtoInputBuilder::p2sh(script.to_vec().into())) + }, + ProtoInputBuilder::p2pkh(script) => { + new_builder(ProtoInputBuilder::p2pkh(script.to_vec().into())) + }, + ProtoInputBuilder::p2wsh(script) => { + new_builder(ProtoInputBuilder::p2wsh(script.to_vec().into())) + }, + ProtoInputBuilder::p2wpkh(script) => { + new_builder(ProtoInputBuilder::p2wpkh(script.to_vec().into())) + }, + ProtoInputBuilder::p2tr_key_path(key_path) => new_builder( + ProtoInputBuilder::p2tr_key_path(Proto::mod_Input::InputTaprootKeyPath { + one_prevout: key_path.one_prevout, + public_key: key_path.public_key.to_vec().into(), + }), + ), + ProtoInputBuilder::p2tr_script_path(script) => new_builder( + ProtoInputBuilder::p2tr_script_path(Proto::mod_Input::InputTaprootScriptPath { + one_prevout: script.one_prevout, + payload: script.payload.to_vec().into(), + control_block: script.control_block.to_vec().into(), + }), + ), + ProtoInputBuilder::brc20_inscribe(brc20) => new_builder( + ProtoInputBuilder::brc20_inscribe(Proto::mod_Input::InputBrc20Inscription { + one_prevout: brc20.one_prevout, + inscribe_to: brc20.inscribe_to.to_vec().into(), + ticker: brc20.ticker.to_string().into(), + transfer_amount: brc20.transfer_amount, + }), + ), + ProtoInputBuilder::ordinal_inscribe(ord) => new_builder( + ProtoInputBuilder::ordinal_inscribe(Proto::mod_Input::InputOrdinalInscription { + one_prevout: ord.one_prevout, + inscribe_to: ord.inscribe_to.to_vec().into(), + mime_type: ord.mime_type.to_string().into(), + payload: ord.payload.to_vec().into(), + }), + ), + ProtoInputBuilder::None => { + return Err(Error::from(Proto::Error::Error_missing_input_builder)) + }, + }, + ProtoInputRecipient::custom_script(custom) => { + ProtoInputRecipient::custom_script(Proto::mod_Input::InputScriptWitness { + script_pubkey: custom.script_pubkey.to_vec().into(), + script_sig: custom.script_sig.to_vec().into(), + witness_items: custom + .witness_items + .iter() + .map(|item| item.to_vec().into()) + .collect(), + signing_method: custom.signing_method, + }) + }, + ProtoInputRecipient::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + Ok(Proto::Input { + private_key: proto.private_key.to_vec().into(), + txid: proto.txid.to_vec().into(), + to_recipient, + ..proto + }) +} + +// Convenience function: our protobuf library wraps certain types (such as +// `bytes`) in `Cow`, but given that calling `clone()` on a `Cow::Borrowed(T)` +// does not actually clone the underlying data (but the smart pointer instead), +// we must hard-clone individual fields manually. This is unfortunately required +// due to how protobuf library works and our use of the 'static constraints. +pub fn hard_clone_proto_output(proto: Proto::Output<'_>) -> Result> { + fn new_builder(variant: ProtoOutputBuilder<'static>) -> ProtoOutputRecipient<'static> { + ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { variant }) + } + + fn new_script_or_hash( + proto: Proto::mod_Output::OutputRedeemScriptOrHash<'_>, + ) -> Result> { + let variant = match proto.variant { + ProtoOutputRedeemScriptOrHashBuilder::redeem_script(script) => { + ProtoOutputRedeemScriptOrHashBuilder::redeem_script(script.to_vec().into()) + }, + ProtoOutputRedeemScriptOrHashBuilder::hash(hash) => { + ProtoOutputRedeemScriptOrHashBuilder::hash(hash.to_vec().into()) + }, + ProtoOutputRedeemScriptOrHashBuilder::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + Ok(Proto::mod_Output::OutputRedeemScriptOrHash { variant }) + } + + fn new_pubkey_or_hash( + proto: Proto::ToPublicKeyOrHash<'_>, + ) -> Result> { + let to_address = match proto.to_address { + ProtoPubkeyOrHash::pubkey(pubkey) => ProtoPubkeyOrHash::pubkey(pubkey.to_vec().into()), + ProtoPubkeyOrHash::hash(hash) => ProtoPubkeyOrHash::hash(hash.to_vec().into()), + ProtoPubkeyOrHash::None => { + return Err(Error::from(Proto::Error::Error_missing_recipient)) + }, + }; + + Ok(Proto::ToPublicKeyOrHash { to_address }) + } + + let to_recipient = match proto.to_recipient { + ProtoOutputRecipient::builder(builder) => match builder.variant { + ProtoOutputBuilder::p2sh(script_or_hash) => new_builder(ProtoOutputBuilder::p2sh( + new_script_or_hash(script_or_hash)?, + )), + ProtoOutputBuilder::p2pkh(pubkey_or_hash) => new_builder(ProtoOutputBuilder::p2pkh( + new_pubkey_or_hash(pubkey_or_hash)?, + )), + ProtoOutputBuilder::p2wsh(script_or_hash) => new_builder(ProtoOutputBuilder::p2wsh( + new_script_or_hash(script_or_hash)?, + )), + ProtoOutputBuilder::p2wpkh(pubkey_or_hash) => new_builder(ProtoOutputBuilder::p2wpkh( + new_pubkey_or_hash(pubkey_or_hash)?, + )), + ProtoOutputBuilder::p2tr_key_path(pubkey) => { + new_builder(ProtoOutputBuilder::p2tr_key_path(pubkey.to_vec().into())) + }, + ProtoOutputBuilder::p2tr_script_path(script_path) => new_builder( + ProtoOutputBuilder::p2tr_script_path(Proto::mod_Output::OutputTaprootScriptPath { + internal_key: script_path.internal_key.to_vec().into(), + merkle_root: script_path.merkle_root.to_vec().into(), + }), + ), + ProtoOutputBuilder::p2tr_dangerous_assume_tweaked(tweaked) => new_builder( + ProtoOutputBuilder::p2tr_dangerous_assume_tweaked(tweaked.to_vec().into()), + ), + ProtoOutputBuilder::brc20_inscribe(brc20) => new_builder( + ProtoOutputBuilder::brc20_inscribe(Proto::mod_Output::OutputBrc20Inscription { + inscribe_to: brc20.inscribe_to.to_vec().into(), + ticker: brc20.ticker.to_string().into(), + transfer_amount: brc20.transfer_amount, + }), + ), + ProtoOutputBuilder::ordinal_inscribe(ord) => new_builder( + ProtoOutputBuilder::ordinal_inscribe(Proto::mod_Output::OutputOrdinalInscription { + inscribe_to: ord.inscribe_to.to_vec().into(), + mime_type: ord.mime_type.to_string().into(), + payload: ord.payload.to_vec().into(), + }), + ), + ProtoOutputBuilder::None => { + return Err(Error::from(Proto::Error::Error_missing_output_builder)) + }, + }, + ProtoOutputRecipient::custom_script_pubkey(custom) => { + ProtoOutputRecipient::custom_script_pubkey(custom.to_vec().into()) + }, + ProtoOutputRecipient::from_address(address) => { + ProtoOutputRecipient::from_address(address.to_string().into()) + }, + ProtoOutputRecipient::None => { + return Err(Error::from(Proto::Error::Error_missing_output_builder)) + }, + }; + + Ok(Proto::Output { + value: proto.value, + to_recipient, + }) +} diff --git a/rust/tw_bitcoin/src/nft.rs b/rust/tw_bitcoin/src/nft.rs deleted file mode 100644 index 8d25b55fd2a..00000000000 --- a/rust/tw_bitcoin/src/nft.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::ordinals::OrdinalsInscription; -use crate::{Recipient, Result}; -use bitcoin::PublicKey; - -pub struct OrdinalNftInscription(OrdinalsInscription); - -impl OrdinalNftInscription { - // Constructs an [Ordinal inscription] with a given MIME type. Common MIME - // types are: - // * "application/json", - // * "application/pdf", - // * "image/gif", - // * "image/jpeg", - // * "image/png", - // * "text/plain;charset=utf-8" - // * ... - // - // [Ordinal inscription]: https://docs.ordinals.com/inscriptions.html - pub fn new(mime_type: &[u8], data: &[u8], recipient: Recipient) -> Result { - OrdinalsInscription::new(mime_type, data, recipient).map(OrdinalNftInscription) - } - pub fn inscription(&self) -> &OrdinalsInscription { - &self.0 - } -} diff --git a/rust/tw_bitcoin/src/output/mod.rs b/rust/tw_bitcoin/src/output/mod.rs deleted file mode 100644 index a93ccda8c87..00000000000 --- a/rust/tw_bitcoin/src/output/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -mod p2pkh; -mod p2tr_key_path; -mod p2tr_script_path; -mod p2wpkh; - -pub use p2pkh::*; -pub use p2tr_key_path::*; -pub use p2tr_script_path::*; -pub use p2wpkh::*; - -#[derive(Debug, Clone)] -pub enum TxOutput { - P2PKH(TxOutputP2PKH), - P2WPKH(TxOutputP2WPKH), - P2TRKeyPath(TxOutputP2TRKeyPath), - P2TRScriptPath(TXOutputP2TRScriptPath), -} - -impl TxOutput { - pub fn satoshis(&self) -> u64 { - match self { - TxOutput::P2PKH(p) => p.satoshis, - TxOutput::P2WPKH(p) => p.satoshis, - TxOutput::P2TRKeyPath(p) => p.satoshis, - TxOutput::P2TRScriptPath(p) => p.satoshis, - } - } -} - -impl From for TxOutput { - fn from(output: TxOutputP2PKH) -> Self { - TxOutput::P2PKH(output) - } -} - -impl From for TxOutput { - fn from(output: TxOutputP2TRKeyPath) -> Self { - TxOutput::P2TRKeyPath(output) - } -} - -impl From for TxOutput { - fn from(output: TxOutputP2WPKH) -> Self { - TxOutput::P2WPKH(output) - } -} - -impl From for TxOutput { - fn from(output: TXOutputP2TRScriptPath) -> Self { - TxOutput::P2TRScriptPath(output) - } -} - -impl From for bitcoin::TxOut { - fn from(out: TxOutput) -> Self { - match out { - TxOutput::P2PKH(p) => Self { - value: p.satoshis, - script_pubkey: p.script_pubkey, - }, - TxOutput::P2WPKH(p) => Self { - value: p.satoshis, - script_pubkey: p.script_pubkey, - }, - TxOutput::P2TRKeyPath(p) => Self { - value: p.satoshis, - script_pubkey: p.script_pubkey, - }, - TxOutput::P2TRScriptPath(p) => Self { - value: p.satoshis, - script_pubkey: p.script_pubkey, - }, - } - } -} diff --git a/rust/tw_bitcoin/src/output/p2pkh.rs b/rust/tw_bitcoin/src/output/p2pkh.rs deleted file mode 100644 index 168d260339d..00000000000 --- a/rust/tw_bitcoin/src/output/p2pkh.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{Error, Recipient, Result}; -use bitcoin::{PubkeyHash, ScriptBuf}; - -#[derive(Debug, Clone)] -pub struct TxOutputP2PKH { - pub(crate) satoshis: u64, - pub(crate) script_pubkey: ScriptBuf, -} - -impl TxOutputP2PKH { - pub fn new(satoshis: u64, recipient: impl Into>) -> Self { - let recipient: Recipient = recipient.into(); - - TxOutputP2PKH { - satoshis, - script_pubkey: ScriptBuf::new_p2pkh(recipient.pubkey_hash()), - } - } - pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { - TxOutputP2PKH { - satoshis, - script_pubkey, - } - } - pub fn builder() -> TxOutputP2PKHBuilder { - TxOutputP2PKHBuilder::new() - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxOutputP2PKHBuilder { - satoshis: Option, - recipient: Option>, -} - -impl TxOutputP2PKHBuilder { - pub fn new() -> TxOutputP2PKHBuilder { - TxOutputP2PKHBuilder { - satoshis: None, - recipient: None, - } - } - pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2PKHBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn recipient( - mut self, - recipient: impl Into>, - ) -> TxOutputP2PKHBuilder { - self.recipient = Some(recipient.into()); - self - } - pub fn build(self) -> Result { - Ok(TxOutputP2PKH::new( - self.satoshis.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/output/p2tr_key_path.rs b/rust/tw_bitcoin/src/output/p2tr_key_path.rs deleted file mode 100644 index 94e166fd4ff..00000000000 --- a/rust/tw_bitcoin/src/output/p2tr_key_path.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::{Error, Recipient, Result}; -use bitcoin::key::TweakedPublicKey; -use bitcoin::script::ScriptBuf; - -#[derive(Debug, Clone)] -pub struct TxOutputP2TRKeyPath { - pub(crate) satoshis: u64, - pub(crate) script_pubkey: ScriptBuf, -} - -impl TxOutputP2TRKeyPath { - pub fn new(satoshis: u64, recipient: Recipient) -> Self { - TxOutputP2TRKeyPath { - satoshis, - script_pubkey: ScriptBuf::new_v1_p2tr_tweaked(recipient.tweaked_pubkey()), - } - } - pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { - TxOutputP2TRKeyPath { - satoshis, - script_pubkey, - } - } - pub fn builder() -> TxOutputP2TRKeyPathBuilder { - TxOutputP2TRKeyPathBuilder::new() - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxOutputP2TRKeyPathBuilder { - satoshis: Option, - recipient: Option>, -} - -impl TxOutputP2TRKeyPathBuilder { - pub fn new() -> Self { - Self::default() - } - pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2TRKeyPathBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn recipient( - mut self, - recipient: impl Into>, - ) -> TxOutputP2TRKeyPathBuilder { - self.recipient = Some(recipient.into()); - self - } - pub fn build(self) -> Result { - Ok(TxOutputP2TRKeyPath::new( - self.satoshis.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/output/p2tr_script_path.rs b/rust/tw_bitcoin/src/output/p2tr_script_path.rs deleted file mode 100644 index cf7e4f55a8c..00000000000 --- a/rust/tw_bitcoin/src/output/p2tr_script_path.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::{Error, Recipient, Result}; -use bitcoin::key::PublicKey; -use bitcoin::script::ScriptBuf; -use bitcoin::secp256k1; -use bitcoin::taproot::{TapNodeHash, TaprootSpendInfo}; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TaprootScript { - pub pubkey: PublicKey, - pub merkle_root: TapNodeHash, -} - -#[derive(Debug, Clone)] -pub struct TaprootProgram { - pub script: ScriptBuf, - pub spend_info: TaprootSpendInfo, -} - -#[derive(Debug, Clone)] -pub struct TXOutputP2TRScriptPath { - pub(crate) satoshis: u64, - pub(crate) script_pubkey: ScriptBuf, -} - -impl TXOutputP2TRScriptPath { - pub fn new(satoshis: u64, recipient: &Recipient) -> Self { - let script_pubkey = ScriptBuf::new_v1_p2tr( - &secp256k1::Secp256k1::new(), - recipient.untweaked_pubkey(), - Some(recipient.merkle_root()), - ); - - TXOutputP2TRScriptPath { - satoshis, - script_pubkey, - } - } - pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { - TXOutputP2TRScriptPath { - satoshis, - script_pubkey, - } - } - pub fn builder() -> TxOutputP2TRScriptPathBuilder { - TxOutputP2TRScriptPathBuilder::new() - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxOutputP2TRScriptPathBuilder { - satoshis: Option, - recipient: Option>, -} - -impl TxOutputP2TRScriptPathBuilder { - pub fn new() -> Self { - Self::default() - } - pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2TRScriptPathBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn recipient( - mut self, - recipient: Recipient, - ) -> TxOutputP2TRScriptPathBuilder { - self.recipient = Some(recipient); - self - } - pub fn build(self) -> Result { - let recipient = self.recipient.ok_or(Error::Todo)?; - Ok(TXOutputP2TRScriptPath::new( - self.satoshis.ok_or(Error::Todo)?, - &recipient, - )) - } -} diff --git a/rust/tw_bitcoin/src/output/p2wpkh.rs b/rust/tw_bitcoin/src/output/p2wpkh.rs deleted file mode 100644 index 82877517874..00000000000 --- a/rust/tw_bitcoin/src/output/p2wpkh.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{Error, Recipient, Result}; -use bitcoin::{ScriptBuf, WPubkeyHash}; - -#[derive(Debug, Clone)] -pub struct TxOutputP2WPKH { - pub(crate) satoshis: u64, - pub(crate) script_pubkey: ScriptBuf, -} - -impl TxOutputP2WPKH { - pub fn new(satoshis: u64, recipient: Recipient) -> Self { - TxOutputP2WPKH { - satoshis, - script_pubkey: ScriptBuf::new_v0_p2wpkh(recipient.wpubkey_hash()), - } - } - pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { - TxOutputP2WPKH { - satoshis, - script_pubkey, - } - } - pub fn builder() -> TxOutputP2WPKHBuilder { - TxOutputP2WPKHBuilder::new() - } -} - -#[derive(Debug, Clone, Default)] -pub struct TxOutputP2WPKHBuilder { - satoshis: Option, - recipient: Option>, -} - -impl TxOutputP2WPKHBuilder { - pub fn new() -> TxOutputP2WPKHBuilder { - Self::default() - } - pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2WPKHBuilder { - self.satoshis = Some(satoshis); - self - } - pub fn recipient(mut self, recipient: Recipient) -> TxOutputP2WPKHBuilder { - self.recipient = Some(recipient); - self - } - pub fn build(self) -> Result { - Ok(TxOutputP2WPKH::new( - self.satoshis.ok_or(Error::Todo)?, - self.recipient.ok_or(Error::Todo)?, - )) - } -} diff --git a/rust/tw_bitcoin/src/recipient.rs b/rust/tw_bitcoin/src/recipient.rs deleted file mode 100644 index f284ac84ab3..00000000000 --- a/rust/tw_bitcoin/src/recipient.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::str::FromStr; - -use crate::output::TaprootScript; -use crate::{tweak_pubkey, Error, Result}; -use bitcoin::key::{KeyPair, PublicKey, TweakedPublicKey, UntweakedPublicKey}; -use bitcoin::taproot::TapNodeHash; -use bitcoin::{ - secp256k1::{self, XOnlyPublicKey}, - Address, Network, PubkeyHash, WPubkeyHash, -}; - -/// This type is used to specify the recipient of a Bitcoin transaction, -/// depending on the required information that's required. For example, P2PKH -/// only requires the public key hash (`Recipient`), while P2TR -/// key-path requires the actual (tweaked) public key (`Recipient`). -/// -/// The recipient can easily downgrade, such as -/// ```rust,ignore -/// let pubkey = Recipient::::from_keypair(keypair); -/// let hash: Recipient = pubkey.into(); -/// ``` -/// -/// But it cannot, for example, derive a public key from just the hash. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Recipient { - inner: T, -} - -impl Recipient { - pub fn from_keypair(keypair: &KeyPair) -> Self { - Recipient { - inner: PublicKey::new(keypair.public_key()), - } - } - pub fn public_key(&self) -> PublicKey { - self.inner - } - pub fn pubkey_hash(&self) -> PubkeyHash { - PubkeyHash::from(self.inner) - } - pub fn wpubkey_hash(&self) -> Result { - self.inner.wpubkey_hash().ok_or(Error::Todo) - } - pub fn tweaked_pubkey(&self) -> TweakedPublicKey { - tweak_pubkey(self.inner) - } - pub fn untweaked_pubkey(&self) -> UntweakedPublicKey { - XOnlyPublicKey::from(self.inner.inner) - } - pub fn legacy_address(&self, network: Network) -> Address { - Address::p2pkh(&self.inner, network) - } - pub fn segwit_address(&self, network: Network) -> Result
{ - Address::p2wpkh(&self.inner, network).map_err(|_| Error::Todo) - } - pub fn taproot_address(&self, network: Network) -> Address { - let untweaked = UntweakedPublicKey::from(self.inner.inner); - Address::p2tr(&secp256k1::Secp256k1::new(), untweaked, None, network) - } - pub fn legacy_address_string(&self, network: Network) -> String { - self.legacy_address(network).to_string() - } - pub fn segwit_address_string(&self, network: Network) -> Result { - self.segwit_address(network).map(|addr| addr.to_string()) - } - pub fn taproot_address_string(&self, network: Network) -> String { - self.taproot_address(network).to_string() - } -} - -impl Recipient { - pub fn pubkey_hash(&self) -> &PubkeyHash { - &self.inner - } -} - -impl Recipient { - pub fn from_slice(slice: &[u8]) -> Result { - Ok(Recipient { - inner: PublicKey::from_slice(slice) - .map_err(|_| Error::Todo)? - .wpubkey_hash() - .ok_or(Error::Todo)?, - }) - } - pub fn wpubkey_hash(&self) -> &WPubkeyHash { - &self.inner - } -} - -impl Recipient { - pub fn from_slice(slice: &[u8]) -> Result { - Ok(Recipient { - inner: PublicKey::from_slice(slice).map_err(|_| Error::Todo)?, - }) - } -} - -impl FromStr for Recipient { - type Err = Error; - - fn from_str(string: &str) -> Result { - Self::from_slice(string.as_bytes()) - } -} - -impl From for Recipient { - fn from(inner: PublicKey) -> Self { - Recipient { inner } - } -} - -impl From for Recipient { - fn from(keypair: KeyPair) -> Self { - Self::from(&keypair) - } -} - -impl From<&KeyPair> for Recipient { - fn from(keypair: &KeyPair) -> Self { - Recipient { - inner: PublicKey::new(keypair.public_key()), - } - } -} - -impl From for Recipient { - fn from(pubkey: PublicKey) -> Self { - let tweaked = tweak_pubkey(pubkey); - Recipient { inner: tweaked } - } -} - -impl From> for Recipient { - fn from(recipient: Recipient) -> Self { - Recipient { - inner: Self::from(recipient.inner).inner, - } - } -} - -impl From for Recipient { - fn from(keypair: KeyPair) -> Self { - Self::from(&keypair) - } -} - -impl From<&KeyPair> for Recipient { - fn from(keypair: &KeyPair) -> Self { - let pk = Recipient::::from(keypair); - let tweaked = Recipient::::from(pk); - - Recipient { - inner: tweaked.inner, - } - } -} - -impl TryFrom for Recipient { - type Error = Error; - - fn try_from(pubkey: PublicKey) -> Result { - Ok(Recipient { - inner: pubkey.wpubkey_hash().unwrap(), - }) - } -} - -impl TryFrom> for Recipient { - type Error = Error; - - fn try_from(recipient: Recipient) -> Result { - Ok(Recipient { - inner: Self::try_from(recipient.inner)?.inner, - }) - } -} - -impl TryFrom<&KeyPair> for Recipient { - type Error = Error; - - fn try_from(keypair: &KeyPair) -> Result { - let pubkey = Recipient::::from(keypair); - Self::try_from(pubkey.inner) - } -} - -impl TryFrom for Recipient { - type Error = Error; - - fn try_from(keypair: KeyPair) -> Result { - Self::try_from(&keypair) - } -} - -impl From for Recipient { - fn from(pubkey: PublicKey) -> Self { - Recipient { - inner: pubkey.into(), - } - } -} - -impl From> for Recipient { - fn from(recipient: Recipient) -> Self { - Recipient { - inner: Self::from(recipient.inner).inner, - } - } -} - -impl From for Recipient { - fn from(keypair: KeyPair) -> Self { - Self::from(&keypair) - } -} - -impl From<&KeyPair> for Recipient { - fn from(keypair: &KeyPair) -> Self { - let pk = Recipient::::from(keypair); - - Recipient { - inner: pk.inner.into(), - } - } -} - -impl Recipient { - pub fn tweaked_pubkey(&self) -> TweakedPublicKey { - self.inner - } -} - -impl Recipient { - pub fn from_keypair(keypair: &KeyPair, merkle_root: TapNodeHash) -> Self { - Recipient { - inner: TaprootScript { - pubkey: PublicKey::new(keypair.public_key()), - merkle_root, - }, - } - } - pub fn from_pubkey_recipient( - recipient: Recipient, - merkle_root: TapNodeHash, - ) -> Self { - Recipient { - inner: TaprootScript { - pubkey: recipient.inner, - merkle_root, - }, - } - } - pub fn untweaked_pubkey(&self) -> UntweakedPublicKey { - XOnlyPublicKey::from(self.inner.pubkey.inner) - } - pub fn merkle_root(&self) -> TapNodeHash { - self.inner.merkle_root - } -} diff --git a/rust/tw_bitcoin/src/tests/address.rs b/rust/tw_bitcoin/src/tests/address.rs deleted file mode 100644 index 032771bb1b7..00000000000 --- a/rust/tw_bitcoin/src/tests/address.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{keypair_from_wif, Recipient}; -use bitcoin::{Network, PublicKey}; - -// This private key was used in a Bitcoin regtest environment. -pub const ALICE_WIF: &str = "cQUNzeMnF9xPPLqZhH7hMVYGwSuu3b78zznuc5UrxgXnYQBq6Bx1"; - -#[test] -fn addresses() { - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let recipient = Recipient::::from(&alice); - - assert_eq!( - recipient.legacy_address_string(Network::Bitcoin), - "1MrZNGN7mfWZiZNQttrzHjfw72jnJC2JNx" - ); - assert_eq!( - recipient.segwit_address_string(Network::Bitcoin).unwrap(), - "bc1qunq74p3h8425hr6wllevlvqqr6sezfxj262rff" - ); - assert_eq!( - recipient.taproot_address_string(Network::Bitcoin), - "bc1pwse34zfpvt344rvlt7tw0ngjtfh9xasc4q03avf0lk74jzjpzjuqaz7ks5" - ); -} diff --git a/rust/tw_bitcoin/src/tests/brc20_transfer.rs b/rust/tw_bitcoin/src/tests/brc20_transfer.rs deleted file mode 100644 index 40b8ff40837..00000000000 --- a/rust/tw_bitcoin/src/tests/brc20_transfer.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::{ - brc20::{BRC20TransferInscription, Ticker}, - keypair_from_wif, TXOutputP2TRScriptPath, TransactionBuilder, TxInputP2TRScriptPath, - TxInputP2WPKH, TxOutputP2WPKH, -}; -use bitcoin::Txid; -use std::str::FromStr; -use tw_encoding::hex; - -// Those private keys were used for Bitcoin mainnet tests and have a transaction -// history. BTC holdings have been emptied. -pub const ALICE_WIF: &str = "L4of5AJ6aKmvChg7gQ7m2RzHFgpWe5Uirmuey1fXJ1FtfmXj59LW"; -pub const BOB_WIF: &str = "L59WHi2hj1HnMAYaFyMqR4Z36HrUDTZQCixzTHachAxbUU9VUCjp"; - -pub const FULL_SATOSHIS: u64 = 26_400; -pub const MINER_FEE: u64 = 3_000; - -pub const BRC20_TICKER: &str = "oadf"; -pub const BRC20_AMOUNT: u64 = 20; -pub const BRC20_INSCRIBE_SATOSHIS: u64 = 7_000; -pub const BRC20_DUST_SATOSHIS: u64 = 546; - -pub const FOR_FEE_SATOSHIS: u64 = FULL_SATOSHIS - BRC20_INSCRIBE_SATOSHIS - MINER_FEE; - -// Used for the committing the Inscription. -// https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 -pub const COMMIT_TXID: &str = "8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008"; -pub const COMMIT_TX_RAW: &str = "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; - -// Used for revealing the Inscription. -// https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca -pub const REVEAL_TXID: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"; -pub const REVEAL_TX_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; - -// Used for transfering the Inscription ("BRC20 transfer"). -// https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 -pub use skip::*; -// We skip formatting for the `skip` module, re-exporting everything. -#[rustfmt::skip] -mod skip { -pub const TRANSFER_TXID_INSCRIPTION: &str = "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca"; -pub const TRANSFER_TXID_FOR_FEES: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"; -pub const TRANSFER_RAW: &str = "02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; -} - -#[test] -fn brc20_transfer() { - let ticker = Ticker::new(BRC20_TICKER.to_string()).unwrap(); - - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let bob = keypair_from_wif(BOB_WIF).unwrap(); - - let txid = Txid::from_str(COMMIT_TXID).unwrap(); - - // # Make "available" tokens "transferable". - // Based on Bitcoin transaction: - // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 - - // Commit transfer. - let input = TxInputP2WPKH::builder() - .txid(txid) - .vout(1) - .recipient(alice.try_into().unwrap()) - .satoshis(FULL_SATOSHIS) - .build() - .unwrap(); - - let transfer = BRC20TransferInscription::new(alice.into(), ticker, BRC20_AMOUNT).unwrap(); - - let output = TXOutputP2TRScriptPath::builder() - .recipient(transfer.inscription().recipient().clone()) - .satoshis(BRC20_INSCRIBE_SATOSHIS) - .build() - .unwrap(); - - let output_change = TxOutputP2WPKH::builder() - .recipient(alice.try_into().unwrap()) - .satoshis(FOR_FEE_SATOSHIS) - .build() - .unwrap(); - - let transaction = TransactionBuilder::new() - .add_input(input.into()) - .add_output(output.into()) - .add_output(output_change.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - // Encode the signed transaction. - let hex = hex::encode(&transaction, false); - assert_eq!(hex, COMMIT_TX_RAW); - - // # Reveal transfer. - // Based on Bitcoin transaction: - // https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca - - let txid = - Txid::from_str("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").unwrap(); - - let input = TxInputP2TRScriptPath::builder() - .txid(txid) - .vout(0) - .recipient(transfer.inscription().recipient().clone()) - .satoshis(BRC20_INSCRIBE_SATOSHIS) - .script(transfer.inscription().taproot_program().to_owned()) - .spend_info(transfer.inscription().spend_info().clone()) - .build() - .unwrap(); - - let output = TxOutputP2WPKH::builder() - .recipient(alice.try_into().unwrap()) - .satoshis(BRC20_DUST_SATOSHIS) - .build() - .unwrap(); - - let transaction = TransactionBuilder::new() - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - // Encode the signed transaction. - let hex = hex::encode(&transaction, false); - - assert_eq!(hex[..164], REVEAL_TX_RAW[..164]); - // We ignore the 64-byte Schnorr signature, since it uses random data for - // signing on each construction and is therefore not reproducible. - assert_ne!(hex[164..292], REVEAL_TX_RAW[164..292]); - assert_eq!(hex[292..], REVEAL_TX_RAW[292..]); - - // # Actually transfer the "transferable" tokens. - // Based on Bitcoin transaction: - // https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 - - // We use a normal P2WPKH output for this. - let input_for_brc20_transfer = TxInputP2WPKH::builder() - .txid(Txid::from_str(TRANSFER_TXID_INSCRIPTION).unwrap()) - .vout(0) - .recipient(alice.try_into().unwrap()) - .satoshis(BRC20_DUST_SATOSHIS) - .build() - .unwrap(); - - let input_for_fee = TxInputP2WPKH::builder() - .txid(Txid::from_str(TRANSFER_TXID_FOR_FEES).unwrap()) - .vout(1) - .recipient(alice.try_into().unwrap()) - .satoshis(FOR_FEE_SATOSHIS) - .build() - .unwrap(); - - // We transfer the tokens to Bob. - let output_brc20_transfer = TxOutputP2WPKH::builder() - .recipient(bob.try_into().unwrap()) - .satoshis(BRC20_DUST_SATOSHIS) - .build() - .unwrap(); - - let output_change = TxOutputP2WPKH::builder() - .recipient(alice.try_into().unwrap()) - .satoshis(FOR_FEE_SATOSHIS - MINER_FEE) - .build() - .unwrap(); - - // We carefully add the BRC20 transfer in the first position for both input and output. - let transaction = TransactionBuilder::new() - .add_input(input_for_brc20_transfer.into()) - .add_input(input_for_fee.into()) - .add_output(output_brc20_transfer.into()) - .add_output(output_change.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - // Encode the signed transaction. - let hex = hex::encode(&transaction, false); - assert_eq!(hex, TRANSFER_RAW); -} diff --git a/rust/tw_bitcoin/src/tests/fee.rs b/rust/tw_bitcoin/src/tests/fee.rs deleted file mode 100644 index f7cfa85d8f9..00000000000 --- a/rust/tw_bitcoin/src/tests/fee.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::calculate_fee; -use bitcoin::{consensus::Decodable, Transaction}; - -// 10 satoshis per virtual byte. -const SAT_VB: u64 = 12; - -fn decode_tx(raw: &str) -> Transaction { - let hex = tw_encoding::hex::decode(raw).unwrap(); - Transaction::consensus_decode(&mut hex.as_slice()).unwrap() -} - -#[test] -fn p2pkh_fee() { - let tx = decode_tx(super::p2pkh::TX_RAW); - - let (weight, fee) = calculate_fee(&tx, SAT_VB); - assert_eq!(weight.to_vbytes_ceil(), 191); - assert_eq!(fee, 191 * SAT_VB); -} - -#[test] -fn p2wpkh_fee() { - let tx = decode_tx(super::p2wpkh::TX_RAW); - - let (weight, fee) = calculate_fee(&tx, SAT_VB); - assert_eq!(weight.to_vbytes_ceil(), 189); - assert_eq!(fee, 189 * SAT_VB); -} - -#[test] -fn brc20_commit_fee() { - // Metadata can be observed live on: - // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 - // - // Fee/VB 19.608 sat/vByte - // Size 235 Bytes - // Weight 610 - - // 19 satoshis per vbyte. - const SAT_19_VB: u64 = 19; - - let tx = decode_tx(super::brc20_transfer::COMMIT_TX_RAW); - - let (weight, fee) = calculate_fee(&tx, SAT_19_VB); - assert_eq!(weight.to_vbytes_ceil(), 153); // 153 = ceil(610/4) - assert_eq!(fee, 153 * SAT_19_VB); -} - -#[test] -fn brc20_reveal_fee() { - // Metadata can be observed live on: - // https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca - // - // Fee/VB 49.267 sat/vByte - // Size 276 Bytes - // Weight 522 - - // 49 satoshis per vbyte (slightly overpaid here...) - const SAT_49_VB: u64 = 49; - - let tx = decode_tx(super::brc20_transfer::REVEAL_TX_RAW); - - let (weight, fee) = calculate_fee(&tx, SAT_49_VB); - assert_eq!(weight.to_vbytes_ceil(), 131); // 131 = ceil(522/4) - assert_eq!(fee, 131 * SAT_49_VB); -} - -#[test] -fn ordinal_nft_commit_fee() { - // Metadata can be observed live on: - // https://www.blockchain.com/explorer/transactions/btc/f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117 - // - // Fee/VB 10.656 sat/vByte - // Size 203 Bytes - // Weight 485 - - // 19 satoshis per vbyte. - const SAT_10_VB: u64 = 10; - - let tx = decode_tx(super::nft::COMMIT_RAW_TX); - - let (weight, fee) = calculate_fee(&tx, SAT_10_VB); - assert_eq!(weight.to_vbytes_ceil(), 122); // 122 = ceil(485/4) - assert_eq!(fee, 122 * SAT_10_VB); -} - -#[test] -fn ordinal_nft_reveal_fee() { - // Metadata can be observed live on: - // https://www.blockchain.com/explorer/transactions/btc/173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac - // - // Fee/VB 15.133 sat/vByte - // Size 7'829 Bytes - // Weight 8'075 - - // 19 satoshis per vbyte. - const SAT_15_VB: u64 = 15; - - let tx = decode_tx(super::nft::REVEAL_RAW_TX); - - let (weight, fee) = calculate_fee(&tx, SAT_15_VB); - assert_eq!(weight.to_vbytes_ceil(), 2019); // 2019 = ceil(8_075/4) - assert_eq!(fee, 2019 * SAT_15_VB); -} diff --git a/rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs b/rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs deleted file mode 100644 index 4c210a85c6a..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::brc20::{BRC20TransferInscription, Ticker}; -use crate::ffi::taproot_build_and_sign_transaction; -use crate::tests::ffi::utils::{ - call_ffi_build_brc20_transfer_script, call_ffi_build_p2wpkh_script, reverse_txid, - ProtoSigningInputBuilder, ProtoTransactionBuilder, -}; -use crate::tests::p2pkh::ALICE_WIF; -use crate::{keypair_from_wif, Recipient, TXOutputP2TRScriptPath}; -use bitcoin::PublicKey; -use std::borrow::Cow; -use tw_encoding::hex; -use tw_proto::Bitcoin::Proto::{TransactionOutput, TransactionVariant}; - -#[test] -fn proto_brc20_transfer_script() { - let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let recipient = Recipient::::from(keypair); - - let satoshis: u64 = 1_000; - let brc20_amount = 20; - let ticker = "oadf"; - - // Call FFI function. - let ffi_out = call_ffi_build_brc20_transfer_script(ticker, brc20_amount, satoshis, &recipient); - - // Compare with native call. - let transfer = BRC20TransferInscription::new( - recipient, - Ticker::new(ticker.to_string()).unwrap(), - brc20_amount, - ) - .unwrap(); - - let tapscript = transfer.inscription().recipient().clone(); - let spending_script = transfer.inscription().taproot_program(); - - let tx_out = TXOutputP2TRScriptPath::new(satoshis, &tapscript); - // Wrap in Protobuf structure. - let proto = TransactionOutput { - value: satoshis as i64, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::from(spending_script.as_bytes()), - }; - - assert_eq!(ffi_out, proto); -} - -/// Commit the Inscription. -#[test] -fn proto_sign_brc20_transfer_inscription_commit() { - use crate::tests::brc20_transfer::*; - - // Prepare keys. - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let alice_privkey = alice.secret_bytes(); - let alice_recipient = Recipient::::from(&alice); - - // Note that the Txid must be reversed. - let txid = reverse_txid(COMMIT_TXID); - - // Build input script. - let input = call_ffi_build_p2wpkh_script(FULL_SATOSHIS, &alice_recipient); - - // Build inscription output. - let output_inscribe = call_ffi_build_brc20_transfer_script( - BRC20_TICKER, - BRC20_AMOUNT, - BRC20_INSCRIBE_SATOSHIS, - &alice_recipient, - ); - - // Build change output. - let output_change = call_ffi_build_p2wpkh_script(FOR_FEE_SATOSHIS, &alice_recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&alice_privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(1) - .script_pubkey(&input.script) - .satoshis(FULL_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output_inscribe.script) - .satoshis(BRC20_INSCRIBE_SATOSHIS) - .variant(TransactionVariant::BRC20TRANSFER) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output_change.script) - .satoshis(FOR_FEE_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - assert_eq!(hex::encode(&signed.encoded, false), COMMIT_TX_RAW); -} - -/// Reveal the Inscription. -#[test] -fn proto_sign_brc20_transfer_inscription_reveal() { - use crate::tests::brc20_transfer::*; - - // Prepare keys. - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let alice_privkey = alice.secret_bytes(); - let alice_recipient = Recipient::::from(&alice); - - // Note that the Txid must be reversed. - let txid = reverse_txid(REVEAL_TXID); - - // Build input script. - let input = call_ffi_build_brc20_transfer_script( - BRC20_TICKER, - BRC20_AMOUNT, - BRC20_INSCRIBE_SATOSHIS, - &alice_recipient, - ); - - // Build inscription output. - let output_p2wpkh = call_ffi_build_p2wpkh_script(BRC20_DUST_SATOSHIS, &alice_recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&alice_privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(0) - .script_pubkey(&input.script) - .satoshis(BRC20_INSCRIBE_SATOSHIS) - .variant(TransactionVariant::BRC20TRANSFER) - // IMPORANT: include the witness containing the actual inscription. - .spending_script(&input.spendingScript) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output_p2wpkh.script) - .satoshis(BRC20_DUST_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - let hex = hex::encode(&signed.encoded, false); - - assert_eq!(hex[..164], REVEAL_TX_RAW[..164]); - // We ignore the 64-byte Schnorr signature, since it uses random data for - // signing on each construction and is therefore not reproducible. - assert_ne!(hex[164..292], REVEAL_TX_RAW[164..292]); - assert_eq!(hex[292..], REVEAL_TX_RAW[292..]); -} - -/// Transfer the Inscription with P2WPKH. -#[test] -fn proto_sign_brc20_transfer_inscription_p2wpkh_transfer() { - use crate::tests::brc20_transfer::*; - - // Prepare keys. - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let alice_privkey = alice.secret_bytes(); - let alice_recipient = Recipient::::from(&alice); - - let bob = keypair_from_wif(BOB_WIF).unwrap(); - let bob_recipient = Recipient::::from(&bob); - - // The Txid to reference the Inscription. - let txid_inscription = reverse_txid(TRANSFER_TXID_INSCRIPTION); - - // The Txid for paying fees. - let txid_for_fees = reverse_txid(TRANSFER_TXID_FOR_FEES); - - // Build input script for Inscription transfer. - let input_transfer = call_ffi_build_p2wpkh_script(BRC20_DUST_SATOSHIS, &alice_recipient); - - // Build input for paying fees. - let input_fees = call_ffi_build_p2wpkh_script(FOR_FEE_SATOSHIS, &alice_recipient); - - // Build Inscription transfer output with Bob as recipient. - let output_transfer = call_ffi_build_p2wpkh_script(BRC20_DUST_SATOSHIS, &bob_recipient); - - // Build change output. - let output_change = - call_ffi_build_p2wpkh_script(FOR_FEE_SATOSHIS - MINER_FEE, &alice_recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&alice_privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid_inscription) - .vout(0) - .script_pubkey(&input_transfer.script) - .satoshis(BRC20_DUST_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .input( - ProtoTransactionBuilder::new() - .txid(&txid_for_fees) - .vout(1) - .script_pubkey(&input_fees.script) - .satoshis(FOR_FEE_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output_transfer.script) - .satoshis(BRC20_DUST_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output_change.script) - .satoshis(FOR_FEE_SATOSHIS - MINER_FEE) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - assert_eq!(hex::encode(&signed.encoded, false), TRANSFER_RAW); -} diff --git a/rust/tw_bitcoin/src/tests/ffi/fees.rs b/rust/tw_bitcoin/src/tests/ffi/fees.rs deleted file mode 100644 index a5cdea4f7b3..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/fees.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::ffi::tw_bitcoin_calculate_transaction_fee; -use tw_memory::ffi::c_result::CUInt64Result; - -/// Convenience wrapper. -fn call_ffi_calculate_fee(hex: &str, sat_vb: u64) -> u64 { - let hex = tw_encoding::hex::decode(hex).unwrap(); - - let res: CUInt64Result = - unsafe { tw_bitcoin_calculate_transaction_fee(hex.as_ptr(), hex.len(), sat_vb) }; - - res.unwrap() -} - -#[test] -fn ffi_calculate_p2pkh_fee() { - let fee = call_ffi_calculate_fee(crate::tests::p2pkh::TX_RAW, 10); - assert_eq!(fee, 191 * 10); -} - -#[test] -fn ffi_calculate_p2wpkh_fee() { - let fee = call_ffi_calculate_fee(crate::tests::p2wpkh::TX_RAW, 10); - assert_eq!(fee, 189 * 10); -} - -#[test] -fn ffi_calculate_brc20_commit_fee() { - let fee = call_ffi_calculate_fee(crate::tests::brc20_transfer::COMMIT_TX_RAW, 19); - assert_eq!(fee, 153 * 19); -} - -#[test] -fn ffi_calculate_brc20_reveal_fee() { - let fee = call_ffi_calculate_fee(crate::tests::brc20_transfer::REVEAL_TX_RAW, 49); - assert_eq!(fee, 131 * 49); -} - -#[test] -fn ffi_calculate_ordinal_nft_commit_fee() { - let fee = call_ffi_calculate_fee(crate::tests::nft::COMMIT_RAW_TX, 10); - assert_eq!(fee, 122 * 10); -} - -#[test] -fn ffi_calculate_ordinal_nft_reveal_fee() { - let fee = call_ffi_calculate_fee(crate::tests::nft::REVEAL_RAW_TX, 15); - assert_eq!(fee, 2019 * 15); -} diff --git a/rust/tw_bitcoin/src/tests/ffi/mod.rs b/rust/tw_bitcoin/src/tests/ffi/mod.rs deleted file mode 100644 index fcb183bbdf0..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod brc20_transfer; -mod fees; -mod nft; -mod scripts; -mod transaction; -mod utils; diff --git a/rust/tw_bitcoin/src/tests/ffi/nft.rs b/rust/tw_bitcoin/src/tests/ffi/nft.rs deleted file mode 100644 index 9a16acd7e31..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/nft.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::ffi::taproot_build_and_sign_transaction; -use crate::nft::OrdinalNftInscription; -use crate::tests::ffi::utils::{ - call_ffi_build_p2wpkh_script, reverse_txid, ProtoSigningInputBuilder, ProtoTransactionBuilder, -}; -use crate::tests::nft::ALICE_WIF; -use crate::{keypair_from_wif, Recipient, TXOutputP2TRScriptPath}; -use bitcoin::PublicKey; -use std::borrow::Cow; -use tw_encoding::hex; -use tw_proto::Bitcoin::Proto::{TransactionOutput, TransactionVariant}; - -use super::utils::call_ffi_build_nft_inscription; - -#[test] -fn proto_nft_inscription_script() { - let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let recipient = Recipient::::from(keypair); - - let mime_type = b"image/png"; - let payload = hex::decode(crate::tests::data::NFT_INSCRIPTION_IMAGE_DATA).unwrap(); - let satoshis: u64 = 1_000; - - // Call FFI function. - let ffi_out = call_ffi_build_nft_inscription(mime_type, &payload, satoshis, &recipient); - - // Compare with native call. - let nft = OrdinalNftInscription::new(mime_type, &payload, recipient).unwrap(); - - let tapscript = nft.inscription().recipient().clone(); - let spending_script = nft.inscription().taproot_program(); - - let tx_out = TXOutputP2TRScriptPath::new(satoshis, &tapscript); - // Wrap in Protobuf structure. - let proto = TransactionOutput { - value: satoshis as i64, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::from(spending_script.as_bytes()), - }; - - assert_eq!(ffi_out, proto); -} - -/// Commit the Inscription. -#[test] -fn proto_sign_nft_inscription_commit() { - use crate::tests::nft::*; - - // Prepare keys. - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let privkey = alice.secret_bytes(); - let recipient = Recipient::::from(&alice); - - let mime_type = b"image/png"; - let payload = hex::decode(crate::tests::data::NFT_INSCRIPTION_IMAGE_DATA).unwrap(); - let satoshis: u64 = 1_000; - - // Note that the Txid must be reversed. - let txid = reverse_txid(COMMIT_TXID); - - // Build input script. - let input = call_ffi_build_p2wpkh_script(FULL_SATOSHIS, &recipient); - - // Build inscription output. - let output = call_ffi_build_nft_inscription(mime_type, &payload, satoshis, &recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(0) - .script_pubkey(&input.script) - .satoshis(FULL_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output.script) - .satoshis(INSCRIBE_SATOSHIS) - .variant(TransactionVariant::NFTINSCRIPTION) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - assert_eq!(hex::encode(&signed.encoded, false), COMMIT_RAW_TX); -} - -/// Reveal the Inscription. -#[test] -fn proto_sign_nft_inscription_reveal() { - use crate::tests::nft::*; - - // Prepare keys. - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let privkey = alice.secret_bytes(); - let recipient = Recipient::::from(&alice); - - let mime_type = b"image/png"; - let payload = hex::decode(crate::tests::data::NFT_INSCRIPTION_IMAGE_DATA).unwrap(); - let satoshis: u64 = 1_000; - - // Note that the Txid must be reversed. - let txid = reverse_txid(REVEAL_TXID); - - // Build inscription input. - let input = call_ffi_build_nft_inscription(mime_type, &payload, satoshis, &recipient); - - // Build inscription output. - let output_p2wpkh = call_ffi_build_p2wpkh_script(DUST_SATOSHIS, &recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(0) - .script_pubkey(&input.script) - .satoshis(INSCRIBE_SATOSHIS) - .variant(TransactionVariant::NFTINSCRIPTION) - // IMPORANT: include the witness containing the actual inscription. - .spending_script(&input.spendingScript) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output_p2wpkh.script) - .satoshis(DUST_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - let hex = hex::encode(&signed.encoded, false); - - assert_eq!(hex[..164], REVEAL_RAW_TX[..164]); - // We ignore the 64-byte Schnorr signature, since it uses random data for - // signing on each construction and is therefore not reproducible. - assert_ne!(hex[164..292], REVEAL_RAW_TX[164..292]); - assert_eq!(hex[292..], REVEAL_RAW_TX[292..]); -} diff --git a/rust/tw_bitcoin/src/tests/ffi/scripts.rs b/rust/tw_bitcoin/src/tests/ffi/scripts.rs deleted file mode 100644 index b0cc8853f7e..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/scripts.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::tests::ffi::utils::{ - call_ffi_build_p2pkh_script, call_ffi_build_p2tr_key_path_script, call_ffi_build_p2wpkh_script, -}; -use crate::tests::p2pkh::ALICE_WIF; -use crate::{keypair_from_wif, Recipient, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH}; -use bitcoin::PublicKey; -use std::borrow::Cow; -use tw_proto::Bitcoin::Proto::TransactionOutput; - -#[test] -fn proto_build_p2pkh_script() { - // Prepare keys. - let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let recipient = Recipient::::from(keypair); - - let satoshis: u64 = 1_000; - - // Call FFI function. - let ffi_out = call_ffi_build_p2pkh_script(satoshis, &recipient); - - // Compare with native call. - let tx_out = TxOutputP2PKH::new(satoshis, recipient); - // Wrap in Protobuf structure. - let proto = TransactionOutput { - value: satoshis as i64, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::default(), - }; - - assert_eq!(ffi_out, proto); -} - -#[test] -fn proto_build_p2wpkh_script() { - // Prepare keys. - let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let recipient = Recipient::::from(keypair); - - let satoshis: u64 = 1_000; - - // Call FFI function. - let ffi_out = call_ffi_build_p2wpkh_script(satoshis, &recipient); - - // Compare with native call. - let tx_out = TxOutputP2WPKH::new(satoshis, recipient.try_into().unwrap()); - // Wrap in Protobuf structure. - let proto = TransactionOutput { - value: satoshis as i64, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::default(), - }; - - assert_eq!(ffi_out, proto); -} - -#[test] -fn proto_build_p2tr_key_path_script() { - // Prepare keys. - let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let recipient = Recipient::::from(keypair); - - let satoshis: u64 = 1_000; - - // Call FFI function. - let ffi_out = call_ffi_build_p2tr_key_path_script(satoshis, &recipient); - - // Compare with native call. - let tx_out = TxOutputP2TRKeyPath::new(satoshis, recipient.try_into().unwrap()); - // Wrap in Protobuf structure. - let proto = TransactionOutput { - value: satoshis as i64, - script: Cow::from(tx_out.script_pubkey.as_bytes()), - spendingScript: Cow::default(), - }; - - assert_eq!(ffi_out, proto); -} diff --git a/rust/tw_bitcoin/src/tests/ffi/transaction.rs b/rust/tw_bitcoin/src/tests/ffi/transaction.rs deleted file mode 100644 index 10d8035d92b..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/transaction.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::ffi::taproot_build_and_sign_transaction; -use crate::tests::ffi::utils::{ - call_ffi_build_p2pkh_script, call_ffi_build_p2tr_key_path_script, call_ffi_build_p2wpkh_script, - reverse_txid, ProtoSigningInputBuilder, ProtoTransactionBuilder, -}; -use crate::{keypair_from_wif, Recipient}; -use bitcoin::PublicKey; -use tw_encoding::hex; -use tw_proto::Bitcoin::Proto::TransactionVariant; - -#[test] -pub fn proto_sign_input_p2pkh_output_p2pkh() { - use crate::tests::p2pkh::*; - - // Prepare keys. - let alice: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let alice_privkey = alice.secret_bytes(); - let alice_recipient = Recipient::::from_keypair(&alice); - - let bob = keypair_from_wif(BOB_WIF).unwrap(); - let bob_recipient = Recipient::::from_keypair(&bob); - - let txid = reverse_txid(TXID); - - // Prepare the scripts. - let input = call_ffi_build_p2pkh_script(FULL_SATOSHIS, &alice_recipient); - let output = call_ffi_build_p2pkh_script(SEND_SATOSHIS, &bob_recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&alice_privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(0) - .script_pubkey(&input.script) - .satoshis(FULL_SATOSHIS) - .variant(TransactionVariant::P2PKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output.script) - .satoshis(SEND_SATOSHIS) - .variant(TransactionVariant::P2PKH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - assert_eq!(hex::encode(&signed.encoded, false), TX_RAW); -} - -#[test] -pub fn proto_sign_input_p2pkh_output_p2wpkh() { - use crate::tests::p2wpkh::*; - - // Prepare keys. - let alice: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let alice_privkey = alice.secret_bytes(); - let alice_recipient = Recipient::::from_keypair(&alice); - - let bob = keypair_from_wif(BOB_WIF).unwrap(); - let bob_recipient = Recipient::::from_keypair(&bob); - - let txid = reverse_txid(TXID); - - // Prepare the scripts. - let input = call_ffi_build_p2pkh_script(FULL_SATOSHIS, &alice_recipient); - let output = call_ffi_build_p2wpkh_script(SEND_SATOSHIS, &bob_recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&alice_privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(0) - .script_pubkey(&input.script) - .satoshis(FULL_SATOSHIS) - .variant(TransactionVariant::P2PKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output.script) - .satoshis(SEND_SATOSHIS) - .variant(TransactionVariant::P2WPKH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - assert_eq!(hex::encode(&signed.encoded, false), TX_RAW); -} - -#[test] -pub fn proto_sign_input_p2pkh_output_p2tr_key_path() { - use crate::tests::p2tr_key_path::*; - - // Prepare keys. - let alice: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); - let alice_privkey = alice.secret_bytes(); - let alice_recipient = Recipient::::from_keypair(&alice); - - let bob = keypair_from_wif(BOB_WIF).unwrap(); - let bob_recipient = Recipient::::from_keypair(&bob); - - let txid = reverse_txid(FIRST_TXID); - - // Prepare the scripts. - let input = call_ffi_build_p2pkh_script(FULL_SATOSHIS, &alice_recipient); - let output = call_ffi_build_p2tr_key_path_script(SEND_SATOSHIS_TO_BOB, &bob_recipient); - - // Construct Protobuf payload. - let signing = ProtoSigningInputBuilder::new() - .private_key(&alice_privkey) - .input( - ProtoTransactionBuilder::new() - .txid(&txid) - .vout(0) - .script_pubkey(&input.script) - .satoshis(FULL_SATOSHIS) - .variant(TransactionVariant::P2PKH) - .build(), - ) - .output( - ProtoTransactionBuilder::new() - .script_pubkey(&output.script) - .satoshis(SEND_SATOSHIS_TO_BOB) - .variant(TransactionVariant::P2TRKEYPATH) - .build(), - ) - .build(); - - let signed = taproot_build_and_sign_transaction(signing).unwrap(); - assert_eq!(hex::encode(&signed.encoded, false), FIRST_TX_RAW); -} diff --git a/rust/tw_bitcoin/src/tests/ffi/utils.rs b/rust/tw_bitcoin/src/tests/ffi/utils.rs deleted file mode 100644 index 67e041c3938..00000000000 --- a/rust/tw_bitcoin/src/tests/ffi/utils.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::ffi::{ - tw_bitcoin_build_nft_inscription, tw_build_brc20_transfer_inscription, tw_build_p2pkh_script, - tw_build_p2tr_key_path_script, tw_build_p2wpkh_script, -}; -use crate::Recipient; -use bitcoin::PublicKey; -use std::borrow::Cow; -use std::ffi::CString; -use tw_proto::Bitcoin::Proto::{ - OutPoint, SigningInput, TransactionOutput, TransactionPlan, TransactionVariant, - UnspentTransaction, -}; - -/// Convenience function for reversing the Txid before it's being passed on to -/// the FFI. -pub fn reverse_txid(txid: &str) -> Vec { - tw_encoding::hex::decode(txid) - .unwrap() - .into_iter() - .rev() - .collect() -} - -/// Convenience wrapper over `tw_build_p2pkh_script` with Protobuf -/// deserialization support. -pub fn call_ffi_build_p2pkh_script<'a, 'b>( - satoshis: u64, - // We use 'b to clarify that `recipient` is not tied to the return value. - recipient: &'b Recipient, -) -> TransactionOutput<'a> { - let pubkey = recipient.public_key().to_bytes(); - - let raw = - unsafe { tw_build_p2pkh_script(satoshis as i64, pubkey.as_ptr(), pubkey.len()).into_vec() }; - - let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); - - // We convert the referenced data into owned data since `raw` goes out of - // scope at the end of the function. - TransactionOutput { - value: des.value, - script: des.script.into_owned().into(), - spendingScript: des.spendingScript.into_owned().into(), - } -} - -/// Convenience wrapper over `tw_build_p2wpkh_script` with Protobuf -/// deserialization support. -pub fn call_ffi_build_p2wpkh_script<'a, 'b>( - satoshis: u64, - // We use 'b to clarify that `recipient` is not tied to the return value. - recipient: &'b Recipient, -) -> TransactionOutput<'a> { - let pubkey = recipient.public_key().to_bytes(); - - let raw = unsafe { - tw_build_p2wpkh_script(satoshis as i64, pubkey.as_ptr(), pubkey.len()).into_vec() - }; - - let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); - - // We convert the referenced data into owned data since `raw` goes out of - // scope at the end of the function. - TransactionOutput { - value: des.value, - script: des.script.into_owned().into(), - spendingScript: des.spendingScript.into_owned().into(), - } -} - -/// Convenience wrapper over `tw_build_p2tr_key_path_script` with Protobuf -/// deserialization support. -pub fn call_ffi_build_p2tr_key_path_script<'a, 'b>( - satoshis: u64, - // We use 'b to clarify that `recipient` is not tied to the return value. - recipient: &'b Recipient, -) -> TransactionOutput<'a> { - let pubkey = recipient.public_key().to_bytes(); - - let raw = unsafe { - tw_build_p2tr_key_path_script(satoshis as i64, pubkey.as_ptr(), pubkey.len()).into_vec() - }; - - let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); - - // We convert the referenced data into owned data since `raw` goes out of - // scope at the end of the function. - TransactionOutput { - value: des.value, - script: des.script.into_owned().into(), - spendingScript: des.spendingScript.into_owned().into(), - } -} - -/// Convenience wrapper over `tw_build_brc20_inscribe_transfer` with Protobuf -/// deserialization support. -pub fn call_ffi_build_brc20_transfer_script<'a, 'b>( - ticker: &str, - brc20_amount: u64, - satoshis: u64, - // We use 'b to clarify that `recipient` is not tied to the return value. - recipient: &'b Recipient, -) -> TransactionOutput<'a> { - let pubkey = recipient.public_key().to_bytes(); - let c_ticker = CString::new(ticker).unwrap(); - - let raw = unsafe { - tw_build_brc20_transfer_inscription( - c_ticker.as_ptr(), - brc20_amount, - satoshis as i64, - pubkey.as_ptr(), - pubkey.len(), - ) - .into_vec() - }; - - let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); - - // We convert the referenced data into owned data since `raw` goes out of - // scope at the end of the function. - TransactionOutput { - value: des.value, - script: des.script.into_owned().into(), - spendingScript: des.spendingScript.into_owned().into(), - } -} - -/// Convenience wrapper over `tw_bitcoin_build_nft_inscription` with Protobuf -/// deserialization support. -pub fn call_ffi_build_nft_inscription<'a, 'b>( - mime_type: &[u8], - data: &[u8], - satoshis: u64, - // We use 'b to clarify that `recipient` is not tied to the return value. - recipient: &'b Recipient, -) -> TransactionOutput<'a> { - let pubkey = recipient.public_key().to_bytes(); - let c_mime_type = CString::new(mime_type).unwrap(); - - let raw = unsafe { - tw_bitcoin_build_nft_inscription( - c_mime_type.as_ptr(), - data.as_ptr(), - data.len(), - satoshis as i64, - pubkey.as_ptr(), - pubkey.len(), - ) - .into_vec() - }; - - let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); - - // We convert the referenced data into owned data since `raw` goes out of - // scope at the end of the function. - TransactionOutput { - value: des.value, - script: des.script.into_owned().into(), - spendingScript: des.spendingScript.into_owned().into(), - } -} - -/// Builder for creating the `SigningInput` Protobuf structure. -pub struct ProtoSigningInputBuilder<'a> { - inner: SigningInput<'a>, -} - -impl<'a> ProtoSigningInputBuilder<'a> { - pub fn new() -> Self { - let signing = SigningInput { - plan: Some(TransactionPlan::default()), - ..Default::default() - }; - - ProtoSigningInputBuilder { inner: signing } - } - pub fn private_key(mut self, privkey: &'a [u8]) -> Self { - self.inner.private_key = vec![Cow::from(privkey)]; - self - } - pub fn input(mut self, tx: UnspentTransaction<'a>) -> Self { - self.inner.utxo.push(tx); - self - } - pub fn output(mut self, tx: UnspentTransaction<'a>) -> Self { - self.inner.plan.as_mut().unwrap().utxos.push(tx); - self - } - pub fn build(self) -> SigningInput<'a> { - self.inner - } -} - -/// Builder for creating the `UnspentTransaction` Protobuf structure. -pub struct ProtoTransactionBuilder<'a> { - inner: UnspentTransaction<'a>, -} - -impl<'a> ProtoTransactionBuilder<'a> { - pub fn new() -> Self { - let unspent = UnspentTransaction { - out_point: Some(OutPoint::default()), - ..Default::default() - }; - - ProtoTransactionBuilder { inner: unspent } - } - pub fn txid(mut self, slice: &'a [u8]) -> Self { - self.inner.out_point.as_mut().unwrap().hash = slice.into(); - self - } - pub fn vout(mut self, vout: u32) -> Self { - self.inner.out_point.as_mut().unwrap().index = vout; - self - } - pub fn variant(mut self, variant: TransactionVariant) -> Self { - self.inner.variant = variant; - self - } - pub fn satoshis(mut self, satoshis: u64) -> Self { - self.inner.amount = satoshis as i64; - self - } - pub fn script_pubkey(mut self, script: &'a [u8]) -> Self { - self.inner.script = script.into(); - self - } - pub fn spending_script(mut self, script: &'a [u8]) -> Self { - self.inner.spendingScript = script.into(); - self - } - pub fn build(self) -> UnspentTransaction<'a> { - self.inner - } -} diff --git a/rust/tw_bitcoin/src/tests/mod.rs b/rust/tw_bitcoin/src/tests/mod.rs deleted file mode 100644 index d95d0a493c7..00000000000 --- a/rust/tw_bitcoin/src/tests/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod address; -mod brc20_transfer; -mod data; -mod fee; -mod ffi; -mod nft; -mod p2pkh; -mod p2tr_key_path; -mod p2wpkh; - -pub const ONE_BTC: u64 = 100_000_000; diff --git a/rust/tw_bitcoin/src/tests/nft.rs b/rust/tw_bitcoin/src/tests/nft.rs deleted file mode 100644 index 20e262307cb..00000000000 --- a/rust/tw_bitcoin/src/tests/nft.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::nft::OrdinalNftInscription; -use crate::{ - keypair_from_wif, TXOutputP2TRScriptPath, TransactionBuilder, TxInputP2TRScriptPath, - TxInputP2WPKH, TxOutputP2WPKH, -}; -use bitcoin::Txid; -use std::str::FromStr; -use tw_encoding::hex; - -pub const ALICE_WIF: &str = "L4of5AJ6aKmvChg7gQ7m2RzHFgpWe5Uirmuey1fXJ1FtfmXj59LW"; -pub const FULL_SATOSHIS: u64 = 32_400; -pub const INSCRIBE_SATOSHIS: u64 = FULL_SATOSHIS - MINER_FEE; -pub const DUST_SATOSHIS: u64 = 546; - -pub const MINER_FEE: u64 = 1_300; - -pub const COMMIT_TXID: &str = "579590c3227253ad423b1e7e3c5b073b8a280d307c68aecd779df2600daa2f99"; -pub const COMMIT_RAW_TX: &str = "02000000000101992faa0d60f29d77cdae687c300d288a3b075b3c7e1e3b42ad537222c39095570000000000ffffffff017c790000000000002251202ac69a7e9dba801e9fcba826055917b84ca6fba4d51a29e47d478de603eedab602473044022054212984443ed4c66fc103d825bfd2da7baf2ab65d286e3c629b36b98cd7debd022050214cfe5d3b12a17aaaf1a196bfeb2f0ad15ffb320c4717eb7614162453e4fe0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; - -pub const REVEAL_TXID: &str = "f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117"; -pub const REVEAL_RAW_TX: &str = super::data::NFT_INSCRIPTION_RAW_HEX; - -#[test] -fn inscribe_nft() { - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - - let payload = hex::decode(super::data::NFT_INSCRIPTION_IMAGE_DATA).unwrap(); - let nft_inscription = OrdinalNftInscription::new(b"image/png", &payload, alice.into()).unwrap(); - - let txid = Txid::from_str(COMMIT_TXID).unwrap(); - - // Commit NFT. - let input = TxInputP2WPKH::builder() - .txid(txid) - .vout(0) - .recipient(alice.try_into().unwrap()) - .satoshis(FULL_SATOSHIS) - .build() - .unwrap(); - - let output = TXOutputP2TRScriptPath::builder() - .recipient(nft_inscription.inscription().recipient().clone()) - .satoshis(INSCRIBE_SATOSHIS) - .build() - .unwrap(); - - let transaction = TransactionBuilder::new() - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&transaction, false); - assert_eq!(hex, COMMIT_RAW_TX); - - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117 - - let txid = Txid::from_str(REVEAL_TXID).unwrap(); - - // Reveal NFT. - let input = TxInputP2TRScriptPath::builder() - .txid(txid) - .vout(0) - .recipient(nft_inscription.inscription().recipient().clone()) - .satoshis(INSCRIBE_SATOSHIS) - .script(nft_inscription.inscription().taproot_program().to_owned()) - .spend_info(nft_inscription.inscription().spend_info().clone()) - .build() - .unwrap(); - - let output = TxOutputP2WPKH::builder() - .recipient(alice.try_into().unwrap()) - .satoshis(DUST_SATOSHIS) - .build() - .unwrap(); - - let transaction = TransactionBuilder::new() - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&transaction, false); - assert_eq!(hex[..164], REVEAL_RAW_TX[..164]); - // We ignore the 64-byte Schnorr signature, since it uses random data for - // signing on each construction and is therefore not reproducible. - assert_ne!(hex[164..292], REVEAL_RAW_TX[164..292]); - assert_eq!(hex[292..], REVEAL_RAW_TX[292..]); - - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac -} diff --git a/rust/tw_bitcoin/src/tests/p2pkh.rs b/rust/tw_bitcoin/src/tests/p2pkh.rs deleted file mode 100644 index 238ba909abf..00000000000 --- a/rust/tw_bitcoin/src/tests/p2pkh.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::*; -use crate::{keypair_from_wif, TransactionBuilder, TxInputP2PKH, TxOutputP2PKH}; -use bitcoin::Txid; -use std::str::FromStr; -use tw_encoding::hex; - -// Those private keys were used in a Bitcoin regtest environment. -pub const ALICE_WIF: &str = "cQUNzeMnF9xPPLqZhH7hMVYGwSuu3b78zznuc5UrxgXnYQBq6Bx1"; -pub const BOB_WIF: &str = "cTk5wSci88FPka7JwHpNEA82dUMjAysdDbCiuYB2fegfgGESAZVn"; -pub const TXID: &str = "1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"; - -pub const FULL_SATOSHIS: u64 = ONE_BTC * 50; -pub const MINER_FEE: u64 = ONE_BTC / 100; -pub const SEND_SATOSHIS: u64 = FULL_SATOSHIS - MINER_FEE; - -// This passed the `bitcoin-cli -retest testmempoolaccept` command. -pub const TX_RAW: &str = "02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000"; - -#[test] -fn sign_input_p2pkh_output_p2pkh() { - // This passed the `bitcoin-cli -retest testmempoolaccept` command. - - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let bob = keypair_from_wif(BOB_WIF).unwrap(); - - // Prepare inputs for Alice. - let input = TxInputP2PKH::builder() - .txid(Txid::from_str(TXID).unwrap()) - .vout(0) - .recipient(alice) - .satoshis(FULL_SATOSHIS) - .build() - .unwrap(); - - // Prepare outputs for Bob. - let output = TxOutputP2PKH::builder() - .satoshis(SEND_SATOSHIS) - .recipient(bob) - .build() - .unwrap(); - - // Alice signs the transaction. - let signed_transaction = TransactionBuilder::new() - .miner_fee(MINER_FEE) - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&signed_transaction, false); - assert_eq!(&hex, TX_RAW); -} diff --git a/rust/tw_bitcoin/src/tests/p2tr_key_path.rs b/rust/tw_bitcoin/src/tests/p2tr_key_path.rs deleted file mode 100644 index cf876fb8975..00000000000 --- a/rust/tw_bitcoin/src/tests/p2tr_key_path.rs +++ /dev/null @@ -1,94 +0,0 @@ -use super::ONE_BTC; -use crate::{ - keypair_from_wif, TransactionBuilder, TxInputP2PKH, TxInputP2TRKeyPath, TxOutputP2TRKeyPath, -}; -use bitcoin::Txid; -use std::str::FromStr; -use tw_encoding::hex; - -// Those private keys were used in a Bitcoin regtest environment. -pub const ALICE_WIF: &str = "cNDFvH3TXCjxgWeVc7vbu4Jw5m2Lu8FkQ69Z2XvFUD9D9rGjofN1"; -pub const BOB_WIF: &str = "cNt3XNHiJdJpoX5zt3CXY8ncgrCted8bxmFBzcGeTZbBw6jkByWB"; - -pub const FULL_SATOSHIS: u64 = ONE_BTC * 50; -pub const MINER_FEE: u64 = ONE_BTC / 100; -pub const SEND_SATOSHIS_TO_BOB: u64 = FULL_SATOSHIS - MINER_FEE; - -// The raw transactions passed the `bitcoin-cli -retest testmempoolaccept` command. -pub const FIRST_TXID: &str = "c50563913e5a838f937c94232f5a8fc74e58b629fae41dfdffcc9a70f833b53a"; -pub const FIRST_TX_RAW: &str = "02000000013ab533f8709accfffd1de4fa29b6584ec78f5a2f23947c938f835a3e916305c5000000006b48304502210086ab2c2192e2738529d6cd9604d8ee75c5b09b0c2f4066a5c5fa3f87a26c0af602202afc7096aaa992235c43e712146057b5ed6a776d82b9129620bc5a21991c0a5301210351e003fdc48e7f31c9bc94996c91f6c3273b7ef4208a1686021bedf7673bb058ffffffff01c0aff62901000000225120e01cfdd05da8fa1d71f987373f3790d45dea9861acb0525c86656fe50f4397a600000000"; - -pub const SEND_SATOSHIS_TO_ALICE: u64 = SEND_SATOSHIS_TO_BOB - MINER_FEE; -pub const SECOND_TXID: &str = "9a582032f6a50cedaff77d3d5604b33adf8bc31bdaef8de977c2187e395860ac"; -pub const SECOND_TX_RAW: &str = "02000000000101ac6058397e18c277e98defda1bc38bdf3ab304563d7df7afed0ca5f63220589a0000000000ffffffff01806de72901000000225120a5c027857e359d19f625e52a106b8ac6ca2d6a8728f6cf2107cd7958ee0787c20140ec2d3910d41506b60aaa20520bb72f15e2d2cbd97e3a8e26ee7bad5f4c56b0f2fb0ceaddac33cb2813a33ba017ba6b1d011bab74a0426f12a2bcf47b4ed5bc8600000000"; - -#[test] -fn sign_input_p2pkh_output_p2tr_key_path() { - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let bob = keypair_from_wif(BOB_WIF).unwrap(); - - // # First transaction: Alice spends the P2PKH coinbase input and creates - // # a P2WPKH output for Bob. - - // Prepare inputs for Alice. - let input = TxInputP2PKH::builder() - .txid(Txid::from_str(FIRST_TXID).unwrap()) - .vout(0) - .recipient(alice) - .satoshis(FULL_SATOSHIS) - .build() - .unwrap(); - - // Prepare outputs for Bob. - let output = TxOutputP2TRKeyPath::builder() - .recipient(bob) - .satoshis(SEND_SATOSHIS_TO_BOB) - .build() - .unwrap(); - - // Alice signs the transaction. - let signed_transaction = TransactionBuilder::new() - .miner_fee(MINER_FEE) - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&signed_transaction, false); - assert_eq!(&hex, FIRST_TX_RAW); - - // # Second transaction: Bob spends the P2WPKH input and creates - // # a P2WPKH output for Alice. - - // Transaction was submitted in regtest env via `sendrawtransaction` and - // mined with `-generate 1` - let input = TxInputP2TRKeyPath::builder() - .txid(Txid::from_str(SECOND_TXID).unwrap()) - .vout(0) - .recipient(bob) - .satoshis(SEND_SATOSHIS_TO_BOB) - .build() - .unwrap(); - - // Prepare outputs for Bob. - let output = TxOutputP2TRKeyPath::builder() - .recipient(alice) - .satoshis(SEND_SATOSHIS_TO_ALICE) - .build() - .unwrap(); - - // Alice signs the transaction. - let signed_transaction = TransactionBuilder::new() - .miner_fee(MINER_FEE) - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(bob) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&signed_transaction, false); - assert_eq!(hex, SECOND_TX_RAW); -} diff --git a/rust/tw_bitcoin/src/tests/p2wpkh.rs b/rust/tw_bitcoin/src/tests/p2wpkh.rs deleted file mode 100644 index 1422e825896..00000000000 --- a/rust/tw_bitcoin/src/tests/p2wpkh.rs +++ /dev/null @@ -1,92 +0,0 @@ -use super::*; -use crate::{keypair_from_wif, TransactionBuilder, TxInputP2PKH, TxInputP2WPKH, TxOutputP2WPKH}; -use bitcoin::Txid; -use std::str::FromStr; -use tw_encoding::hex; - -// Those private keys were used in a Bitcoin regtest environment. -pub const ALICE_WIF: &str = "cQX5ePcXjTx7C5p6xV8zkp2NN9unhZx4a8RQVPiHd52WxoApV6yK"; -pub const BOB_WIF: &str = "cMn7SSCtE5yt2PS97P4NCMvxpCVvT4cBuHiCzKFW5XMvio4fQbD1"; -pub const TXID: &str = "181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911"; - -pub const FULL_SATOSHIS: u64 = ONE_BTC * 50; -pub const MINER_FEE: u64 = ONE_BTC / 100; -pub const SEND_SATOSHIS: u64 = FULL_SATOSHIS - MINER_FEE; - -// This passed the `bitcoin-cli -retest testmempoolaccept` command. -pub const TX_RAW: &str = "020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000"; - -#[test] -fn sign_input_p2pkh_and_p2wpkh_output_p2wpkh() { - let alice = keypair_from_wif(ALICE_WIF).unwrap(); - let bob = keypair_from_wif(BOB_WIF).unwrap(); - - // # First transaction: Alice spends the P2PKH coinbase input and creates - // # a P2WPKH output for Bob. - - // Prepare inputs for Alice. - let input = TxInputP2PKH::builder() - .txid(Txid::from_str(TXID).unwrap()) - .vout(0) - .recipient(alice) - .satoshis(FULL_SATOSHIS) - .build() - .unwrap(); - - // Prepare outputs for Bob. - let output = TxOutputP2WPKH::builder() - .recipient(bob.try_into().unwrap()) - .satoshis(SEND_SATOSHIS) - .build() - .unwrap(); - - // Alice signs the transaction. - let signed_transaction = TransactionBuilder::new() - .miner_fee(MINER_FEE) - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(alice) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&signed_transaction, false); - assert_eq!(&hex, TX_RAW); - - // # Second transaction: Bob spends the P2WPKH input and creates - // # a P2WPKH output for Alice. - - // Transaction was submitted in regtest env via `sendrawtransaction` and - // mined with `-generate 1` - const TX_RAW_SECOND: &str = "020000000001016e1f16dcfafbb3a83697f6c23c624cd71085a7f8a25ce0bd9743a41d0a458e850000000000ffffffff01806de7290100000016001460cda7b50f14c152d7401c28ae773c698db9237302483045022100a9b517de5a5e036d7133df499b5b751db6f9a01576a6c5dc38229ec08b6c45cd02200e42c9f8c707c9bf0ceab4f739ec8d683dc1f1f29e195a8da9bc183584d624a60121025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f00000000"; - const LATEST_TXID: &str = "858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e"; - const SEND_TO_ALICE: u64 = SEND_SATOSHIS - MINER_FEE; - - let input = TxInputP2WPKH::builder() - .txid(Txid::from_str(LATEST_TXID).unwrap()) - .vout(0) - .recipient(bob.try_into().unwrap()) - .satoshis(SEND_SATOSHIS) - .build() - .unwrap(); - - // Prepare outputs for Bob. - let output = TxOutputP2WPKH::builder() - .recipient(alice.try_into().unwrap()) - .satoshis(SEND_TO_ALICE) - .build() - .unwrap(); - - // Alice signs the transaction. - let signed_transaction = TransactionBuilder::new() - .miner_fee(MINER_FEE) - .add_input(input.into()) - .add_output(output.into()) - .sign_inputs(bob) - .unwrap() - .serialize() - .unwrap(); - - let hex = hex::encode(&signed_transaction, false); - assert_eq!(&hex, TX_RAW_SECOND); -} diff --git a/rust/tw_bitcoin/src/transaction.rs b/rust/tw_bitcoin/src/transaction.rs deleted file mode 100644 index 7706f85eca8..00000000000 --- a/rust/tw_bitcoin/src/transaction.rs +++ /dev/null @@ -1,255 +0,0 @@ -use crate::claim::{ClaimLocation, TransactionSigner}; -use crate::input::*; -use crate::output::*; -use crate::{Error, Result}; -use bitcoin::blockdata::locktime::absolute::{Height, LockTime}; -use bitcoin::consensus::Encodable; -use bitcoin::sighash::{EcdsaSighashType, SighashCache, TapSighashType}; -use bitcoin::taproot::{LeafVersion, TapLeafHash}; -use bitcoin::{secp256k1, Address, TxIn, TxOut}; -use bitcoin::{Transaction, Weight}; - -/// Determines the weight of the transaction and calculates the fee with the -/// given satoshis per vbyte. -pub fn calculate_fee(tx: &Transaction, sat_vb: u64) -> (Weight, u64) { - let weight = tx.weight(); - (weight, weight.to_vbytes_ceil() * sat_vb) -} - -#[derive(Debug, Clone)] -pub struct TransactionBuilder { - pub version: i32, - pub lock_time: LockTime, - inputs: Vec, - outputs: Vec, - miner_fee: Option, - return_address: Option
, - contains_taproot: bool, -} - -impl Default for TransactionBuilder { - fn default() -> Self { - TransactionBuilder { - version: 2, - // No lock time, transaction is immediately spendable. - lock_time: LockTime::Blocks(Height::ZERO), - inputs: vec![], - outputs: vec![], - miner_fee: None, - return_address: None, - contains_taproot: false, - } - } -} - -impl TransactionBuilder { - pub fn new() -> Self { - Self::default() - } - pub fn version(mut self, version: i32) -> Self { - self.version = version; - self - } - pub fn lock_time_height(mut self, height: u32) -> Result { - self.lock_time = LockTime::Blocks(Height::from_consensus(height).map_err(|_| Error::Todo)?); - Ok(self) - } - pub fn return_address(mut self, address: Address) -> Self { - self.return_address = Some(address); - self - } - pub fn miner_fee(mut self, satoshis: u64) -> Self { - self.miner_fee = Some(satoshis); - self - } - pub fn add_input(mut self, input: TxInput) -> Self { - match input { - TxInput::P2TRKeyPath(_) | TxInput::P2TRScriptPath(_) => self.contains_taproot = true, - _ => {}, - } - - self.inputs.push(input); - self - } - pub fn add_output(mut self, output: TxOutput) -> Self { - self.outputs.push(output); - self - } - pub fn sign_inputs(self, signer: S) -> Result - where - S: TransactionSigner, - { - self.sign_inputs_fn(|input, sighash| match input { - TxInput::P2PKH(p) => signer - .claim_p2pkh(p, sighash, EcdsaSighashType::All) - .map(|claim| ClaimLocation::Script(claim.0)), - TxInput::P2WPKH(p) => signer - .claim_p2wpkh(p, sighash, EcdsaSighashType::All) - .map(|claim| ClaimLocation::Witness(claim.0)), - TxInput::P2TRKeyPath(p) => signer - .claim_p2tr_key_path(p, sighash, TapSighashType::Default) - .map(|claim| ClaimLocation::Witness(claim.0)), - TxInput::P2TRScriptPath(p) => signer - .claim_p2tr_script_path(p, sighash, TapSighashType::Default) - .map(|claim| ClaimLocation::Witness(claim.0)), - }) - } - pub fn sign_inputs_fn(self, signer: F) -> Result - where - F: Fn(&TxInput, secp256k1::Message) -> Result, - { - // Prepare boilerplate transaction for `bitcoin` crate. - let mut tx = Transaction { - version: self.version, - lock_time: self.lock_time, - input: vec![], - output: vec![], - }; - - // Prepare the inputs for `bitcoin` crate. - for input in self.inputs.iter().cloned() { - let btxin = TxIn::from(input); - tx.input.push(btxin); - } - - // Prepare the outputs for `bitcoin` crate. - for output in self.outputs.iter().cloned() { - let btc_txout = TxOut::from(output); - tx.output.push(btc_txout); - } - - // Satoshi output check - /* - // TODO: This should be enabled, eventually. - let miner_fee = self.miner_fee.ok_or(Error::Todo)?; - if total_satoshis_outputs + miner_fee > total_satoshi_inputs { - return Err(Error::Todo); - } - */ - - // If Taproot is enabled, we prepare the full `TxOuts` (value and - // scriptPubKey) for hashing, which will then be signed. What - // distinguishes this from legacy signing is that the output value in - // satoshis is actually part of the signature. - let mut prevouts = vec![]; - if self.contains_taproot { - for input in &self.inputs { - prevouts.push(TxOut { - value: input.ctx().value, - script_pubkey: input.ctx().script_pubkey.clone(), - }); - } - } - - let mut cache = SighashCache::new(tx); - - let mut claims = vec![]; - - // For each input (index), we create a hash which is to be signed. - for (index, input) in self.inputs.iter().enumerate() { - match input { - TxInput::P2PKH(p2pkh) => { - let hash = cache - .legacy_signature_hash( - index, - &p2pkh.ctx().script_pubkey, - EcdsaSighashType::All.to_u32(), - ) - .map_err(|_| Error::Todo)?; - - let message = secp256k1::Message::from_slice(hash.as_ref()) - .expect("Sighash must always convert to secp256k1::Message"); - let updated = signer(input, message)?; - - claims.push((index, updated)); - }, - TxInput::P2WPKH(p2wpkh) => { - let hash = cache - .segwit_signature_hash( - index, - p2wpkh - .ctx() - .script_pubkey - .p2wpkh_script_code() - .as_ref() - .expect("P2WPKH builder must set the script code correctly"), - p2wpkh.ctx().value, - EcdsaSighashType::All, - ) - .map_err(|_| Error::Todo)?; - - let message = secp256k1::Message::from_slice(hash.as_ref()) - .expect("Sighash must always convert to secp256k1::Message"); - let updated = signer(input, message)?; - - claims.push((index, updated)); - }, - TxInput::P2TRKeyPath(_) => { - let hash = cache - .taproot_key_spend_signature_hash( - index, - &bitcoin::sighash::Prevouts::All(&prevouts), - TapSighashType::Default, - ) - .map_err(|_| Error::Todo)?; - - let message = secp256k1::Message::from_slice(hash.as_ref()) - .expect("Sighash must always convert to secp256k1::Message"); - let updated = signer(input, message)?; - - claims.push((index, updated)); - }, - TxInput::P2TRScriptPath(p2trsp) => { - let leaf_hash = - TapLeafHash::from_script(p2trsp.witness(), LeafVersion::TapScript); - - let hash = cache - .taproot_script_spend_signature_hash( - index, - &bitcoin::sighash::Prevouts::All(&prevouts), - leaf_hash, - TapSighashType::Default, - ) - .map_err(|_| Error::Todo)?; - - let message = secp256k1::Message::from_slice(hash.as_ref()) - .expect("Sighash must always convert to secp256k1::Message"); - let updated = signer(input, message)?; - - claims.push((index, updated)); - }, - }; - } - - let mut tx = cache.into_transaction(); - - // Update the transaction with the updated scriptSig/Witness. - for (index, claim_loc) in claims { - match claim_loc { - ClaimLocation::Script(script) => { - tx.input[index].script_sig = script; - }, - ClaimLocation::Witness(witness) => { - tx.input[index].witness = witness; - }, - } - } - - Ok(TransactionSigned { inner: tx }) - } -} - -pub struct TransactionSigned { - pub inner: Transaction, -} - -impl TransactionSigned { - pub fn serialize(&self) -> Result> { - let mut buffer = vec![]; - self.inner - .consensus_encode(&mut buffer) - .map_err(|_| Error::Todo)?; - - Ok(buffer) - } -} diff --git a/rust/tw_bitcoin/src/utils.rs b/rust/tw_bitcoin/src/utils.rs deleted file mode 100644 index 399a84526e5..00000000000 --- a/rust/tw_bitcoin/src/utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::{Error, Result}; -use bitcoin::key::{KeyPair, PrivateKey, PublicKey, TapTweak, TweakedPublicKey}; -use bitcoin::secp256k1::{self, XOnlyPublicKey}; - -pub fn keypair_from_wif(string: &str) -> Result { - let pk = PrivateKey::from_wif(string).map_err(|_| Error::Todo)?; - let keypair = KeyPair::from_secret_key(&secp256k1::Secp256k1::new(), &pk.inner); - Ok(keypair) -} - -pub(crate) fn tweak_pubkey(pubkey: PublicKey) -> TweakedPublicKey { - let xonly = XOnlyPublicKey::from(pubkey.inner); - let (tweaked, _) = xonly.tap_tweak(&secp256k1::Secp256k1::new(), None); - tweaked -} diff --git a/rust/tw_bitcoin/tests/brc20.rs b/rust/tw_bitcoin/tests/brc20.rs new file mode 100644 index 00000000000..055452b5942 --- /dev/null +++ b/rust/tw_bitcoin/tests/brc20.rs @@ -0,0 +1,137 @@ +mod common; + +use common::hex; +use tw_bitcoin::aliases::*; +use tw_bitcoin::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_sign_brc20_commit_reveal_transfer() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + let alice_pubkey = hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); + + let txid: Vec = hex("8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 1, + value: 26_400, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 7_000, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::brc20_inscribe( + Proto::mod_Output::OutputBrc20Inscription { + inscribe_to: alice_pubkey.as_slice().into(), + ticker: "oadf".into(), + transfer_amount: 20, + }, + ), + }), + }; + + // Change/return transaction. + let out2 = Proto::Output { + value: 16_400, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1, out2], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!( + signed.txid, + hex("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + ); + + let encoded = tw_encoding::hex::encode(signed.encoded, false); + let transaction = signed.transaction.unwrap(); + + assert_eq!(transaction.inputs.len(), 1); + assert_eq!(transaction.outputs.len(), 2); + assert_eq!(&encoded, "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + + // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 + let txid: Vec = hex("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 7_000, + sighash_type: UtxoProto::SighashType::UseDefault, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::brc20_inscribe(Proto::mod_Input::InputBrc20Inscription { + one_prevout: false, + inscribe_to: alice_pubkey.as_slice().into(), + ticker: "oadf".into(), + transfer_amount: 20, + }), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 546, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + // We enable deterministic Schnorr signatures here + dangerous_use_fixed_schnorr_rng: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + // https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca + assert_eq!( + signed.txid, + hex("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") + ); + + let encoded = tw_encoding::hex::encode(signed.encoded, false); + let transaction = signed.transaction.unwrap(); + + assert_eq!(encoded, "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03406a35548b8fa4620028e021a944c1d3dc6e947243a7bfc901bf63fefae0d2460efa149a6440cab51966aa4f09faef2d1e5efcba23ab4ca6e669da598022dbcfe35b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + assert_eq!(transaction.inputs.len(), 1); + assert_eq!(transaction.outputs.len(), 1); +} diff --git a/rust/tw_bitcoin/src/tests/data.rs b/rust/tw_bitcoin/tests/common/data.rs similarity index 99% rename from rust/tw_bitcoin/src/tests/data.rs rename to rust/tw_bitcoin/tests/common/data.rs index 4ea13a2b2dd..c17d4f0113b 100644 --- a/rust/tw_bitcoin/src/tests/data.rs +++ b/rust/tw_bitcoin/tests/common/data.rs @@ -256,9 +256,9 @@ d1d75f32de1ddd2b7e64faa36f9bd0caa3fa7df3b7fb9b3e92bb7f6d48a9\ pub const NFT_INSCRIPTION_RAW_HEX: &str = "\ 020000000001011771decbce2766b39d8fe66f4dc11737b3146c71f8cc6a\ e1397384c5e508e7f10000000000ffffffff012202000000000000160014\ -e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340cc1e7b0b5fa18b28\ -dce702e4e8ed2e91069d682b8daa3a773774bfc7d0e6f737d403016a9016\ -b58a92592ad0b41682e6209167444eb56605532b28e9be922d3afdda1d00\ +e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340e9bb1aaf8cb98c60\ +f793f08fc4258a9ad5a7b430fc5906fe3b522b67431fec087cbaf54767d4\ +3061be51d8f49fa179cb9b4f0f9e00ee23101962ac64f9d71a76fdda1d00\ 63036f7264010109696d6167652f706e67004d080289504e470d0a1a0a00\ 00000d4948445200000360000002be0803000000f30f8d7d000000d8504c\ 54450000003070bf3070af3173bd3078b73870b73070b73575ba3075ba30\ diff --git a/rust/tw_bitcoin/tests/common/mod.rs b/rust/tw_bitcoin/tests/common/mod.rs new file mode 100644 index 00000000000..9afe54f6d4f --- /dev/null +++ b/rust/tw_bitcoin/tests/common/mod.rs @@ -0,0 +1,12 @@ +// This seems to be required, even if the tests in `tests/` actually use +// functions/constants. +#![allow(dead_code)] + +pub mod data; + +pub const ONE_BTC: u64 = 100_000_000; +pub const MINER_FEE: u64 = 1_000_000; + +pub fn hex(string: &str) -> Vec { + tw_encoding::hex::decode(string).unwrap() +} diff --git a/rust/tw_bitcoin/tests/free_estimate.rs b/rust/tw_bitcoin/tests/free_estimate.rs new file mode 100644 index 00000000000..3e86653ce6d --- /dev/null +++ b/rust/tw_bitcoin/tests/free_estimate.rs @@ -0,0 +1,246 @@ +mod common; + +use common::{hex, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +const SAT_VB: u64 = 20; + +#[test] +fn p2pkh_fee_estimate() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let mut signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![], + outputs: vec![], + input_selector: UtxoProto::InputSelector::UseAll, + fee_per_vb: SAT_VB, + disable_change_output: true, + ..Default::default() + }; + + signing.inputs.push(Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 2 * ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }); + + signing.outputs.push(Proto::Output { + value: ONE_BTC, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }); + + let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); + assert_eq!(prehashes.error, Proto::Error::OK); + assert_eq!(prehashes.weight_estimate, 768); + assert_eq!(prehashes.fee_estimate, (768 + 3) / 4 * SAT_VB); + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(signed.weight, 768); + assert_eq!(signed.fee, (768 + 3) / 4 * SAT_VB); +} + +#[test] +fn p2wpkh_fee_estimate() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let mut signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![], + outputs: vec![], + input_selector: UtxoProto::InputSelector::UseAll, + fee_per_vb: SAT_VB, + disable_change_output: true, + ..Default::default() + }; + + signing.inputs.push(Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 2 * ONE_BTC, + sequence: u32::MAX, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }); + + signing.outputs.push(Proto::Output { + value: ONE_BTC, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }); + + let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); + assert_eq!(prehashes.error, Proto::Error::OK); + // TODO: The estimated weight/fee is slightly off from the finalized + // weight/fee. This is probably good enough, but we can probably improve + // this. + assert_eq!(prehashes.weight_estimate, 436); + assert_eq!(prehashes.fee_estimate, (436 + 3) / 4 * SAT_VB); + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(signed.weight, 438); + assert_eq!(signed.fee, (438 + 3) / 4 * SAT_VB); +} + +#[test] +fn p2tr_key_path_fee_estimate() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 2 * ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2tr_key_path(Proto::mod_Input::InputTaprootKeyPath { + one_prevout: false, + public_key: alice_pubkey.as_slice().into(), + }), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2tr_key_path(bob_pubkey.as_slice().into()), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + fee_per_vb: SAT_VB, + disable_change_output: true, + ..Default::default() + }; + + let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); + assert_eq!(prehashes.error, Proto::Error::OK); + // TODO: The estimated weight/fee is slightly off from the finalized + // weight/fee. This is probably good enough, but we can probably improve + // this. + assert_eq!(prehashes.weight_estimate, 450); + assert_eq!(prehashes.fee_estimate, (450 + 3) / 4 * SAT_VB); + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(signed.weight, 445); + assert_eq!(signed.fee, (445 + 3) / 4 * SAT_VB); +} + +#[test] +fn brc20_inscribe_fee_estimate() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 2 * ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::brc20_inscribe(Proto::mod_Input::InputBrc20Inscription { + one_prevout: false, + inscribe_to: alice_pubkey.as_slice().into(), + ticker: "oadf".into(), + transfer_amount: 20, + }), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::brc20_inscribe( + Proto::mod_Output::OutputBrc20Inscription { + inscribe_to: alice_pubkey.as_slice().into(), + ticker: "oadf".into(), + transfer_amount: 20, + }, + ), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + fee_per_vb: SAT_VB, + disable_change_output: true, + ..Default::default() + }; + + let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); + assert_eq!(prehashes.error, Proto::Error::OK); + // TODO: The estimated weight/fee is slightly off from the finalized + // weight/fee. This is probably good enough, but we can probably improve + // this. + assert_eq!(prehashes.weight_estimate, 575); + assert_eq!(prehashes.fee_estimate, (575 + 3) / 4 * SAT_VB); + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(signed.weight, 571); + assert_eq!(signed.fee, (571 + 3) / 4 * SAT_VB); +} diff --git a/rust/tw_bitcoin/tests/legacy_build_sign.rs b/rust/tw_bitcoin/tests/legacy_build_sign.rs new file mode 100644 index 00000000000..4c871fb0dc9 --- /dev/null +++ b/rust/tw_bitcoin/tests/legacy_build_sign.rs @@ -0,0 +1,445 @@ +#![allow(deprecated)] + +mod common; + +use common::{hex, MINER_FEE, ONE_BTC}; +use secp256k1::ffi::CPtr; +use std::ffi::CString; +use tw_proto::Bitcoin::Proto as LegacyProto; +use tw_proto::Common::Proto as CommonProto; +use wallet_core_rs::ffi::bitcoin::legacy as legacy_ffi; + +const ONE_BTC_I64: i64 = ONE_BTC as i64; +const MINER_FEE_I64: i64 = MINER_FEE as i64; + +#[test] +fn ffi_proto_sign_input_p2pkh_output_p2pkh() { + let alice_private_key = hex("56429688a1a6b00b90ccd22a0de0a376b6569d8684022ae92229a28478bfb657"); + let bob_pubkey = hex("037ed9a436e11ec4947ac4b7823787e24ba73180f1edd2857bff19c9f4d62b65bf"); + + let txid = hex("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b") + .into_iter() + .rev() + .collect(); + + // Output. + let output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2pkh_script( + ONE_BTC_I64 * 50 - MINER_FEE_I64, + bob_pubkey.as_c_ptr(), + bob_pubkey.len(), + ) + .into_vec() + }; + let output: LegacyProto::TransactionOutput = tw_proto::deserialize(&output).unwrap(); + + // Prepare SigningInput. + let signing = LegacyProto::SigningInput { + private_key: vec![alice_private_key.into()], + utxo: vec![LegacyProto::UnspentTransaction { + out_point: Some(LegacyProto::OutPoint { + hash: txid, + index: 0, + sequence: u32::MAX, + ..Default::default() + }), + // For inputs, script is not needed (derived from variant). + script: Default::default(), + amount: ONE_BTC_I64 * 50, + variant: LegacyProto::TransactionVariant::P2PKH, + spendingScript: Default::default(), + }], + plan: Some(LegacyProto::TransactionPlan { + utxos: vec![LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: output.script, + amount: output.value, + variant: LegacyProto::TransactionVariant::P2PKH, + spendingScript: Default::default(), + }], + ..Default::default() + }), + ..Default::default() + }; + let serialized = tw_proto::serialize(&signing).unwrap(); + + // Sign and build the transaction. + let signed = unsafe { + legacy_ffi::tw_bitcoin_legacy_taproot_build_and_sign_transaction( + serialized.as_c_ptr(), + serialized.len(), + ) + .into_vec() + }; + let signed: LegacyProto::SigningOutput = tw_proto::deserialize(&signed).unwrap(); + + // Check result. + assert_eq!(signed.error, CommonProto::SigningError::OK); + let encoded_hex = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(encoded_hex, "02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000"); +} + +#[test] +fn ffi_proto_sign_input_p2pkh_output_p2wpkh() { + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + let txid = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + // Output. + let output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2wpkh_script( + ONE_BTC_I64 * 50 - MINER_FEE_I64, + bob_pubkey.as_c_ptr(), + bob_pubkey.len(), + ) + .into_vec() + }; + let output: LegacyProto::TransactionOutput = tw_proto::deserialize(&output).unwrap(); + + // Prepare SigningInput. + let signing = LegacyProto::SigningInput { + private_key: vec![alice_private_key.into()], + utxo: vec![LegacyProto::UnspentTransaction { + out_point: Some(LegacyProto::OutPoint { + hash: txid, + index: 0, + sequence: u32::MAX, + ..Default::default() + }), + // For inputs, script is not needed (derived from variant). + script: Default::default(), + amount: ONE_BTC_I64 * 50, + variant: LegacyProto::TransactionVariant::P2PKH, + spendingScript: Default::default(), + }], + plan: Some(LegacyProto::TransactionPlan { + utxos: vec![LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: output.script, + amount: output.value, + variant: LegacyProto::TransactionVariant::P2WPKH, + spendingScript: Default::default(), + }], + ..Default::default() + }), + ..Default::default() + }; + let serialized = tw_proto::serialize(&signing).unwrap(); + + // Sign and build the transaction. + let signed = unsafe { + legacy_ffi::tw_bitcoin_legacy_taproot_build_and_sign_transaction( + serialized.as_c_ptr(), + serialized.len(), + ) + .into_vec() + }; + let signed: LegacyProto::SigningOutput = tw_proto::deserialize(&signed).unwrap(); + + // Check result. + assert_eq!(signed.error, CommonProto::SigningError::OK); + let encoded_hex = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(encoded_hex, "020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000"); +} + +#[test] +fn ffi_proto_sign_input_p2pkh_output_p2tr_key_path() { + let alice_private_key = hex("12ce558df23528f1aa86f1f51ac7e13a197a06bda27610fa89e13b04c40ee999"); + let alice_pubkey = hex("0351e003fdc48e7f31c9bc94996c91f6c3273b7ef4208a1686021bedf7673bb058"); + let bob_private_key = hex("26c2566adcc030a1799213bfd546e615f6ab06f72085ec6806ff1761da48d227"); + let bob_pubkey = hex("02c0938cf377023dfde55e9c96b3cff4ca8894fb6b5d2009006bd43c0bff69cac9"); + + let txid = hex("c50563913e5a838f937c94232f5a8fc74e58b629fae41dfdffcc9a70f833b53a") + .into_iter() + .rev() + .collect(); + + // Output. + let output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2tr_key_path_script( + ONE_BTC_I64 * 50 - MINER_FEE_I64, + bob_pubkey.as_c_ptr(), + bob_pubkey.len(), + ) + .into_vec() + }; + let output: LegacyProto::TransactionOutput = tw_proto::deserialize(&output).unwrap(); + + // Prepare SigningInput. + let signing = LegacyProto::SigningInput { + private_key: vec![alice_private_key.into()], + utxo: vec![LegacyProto::UnspentTransaction { + out_point: Some(LegacyProto::OutPoint { + hash: txid, + index: 0, + sequence: u32::MAX, + ..Default::default() + }), + // For inputs, script is not needed (derived from variant). + script: Default::default(), + amount: ONE_BTC_I64 * 50, + variant: LegacyProto::TransactionVariant::P2PKH, + spendingScript: Default::default(), + }], + plan: Some(LegacyProto::TransactionPlan { + utxos: vec![LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: output.script, + amount: output.value, + variant: LegacyProto::TransactionVariant::P2TRKEYPATH, + spendingScript: Default::default(), + }], + ..Default::default() + }), + ..Default::default() + }; + let serialized = tw_proto::serialize(&signing).unwrap(); + + // Sign and build the transaction. + let signed = unsafe { + legacy_ffi::tw_bitcoin_legacy_taproot_build_and_sign_transaction( + serialized.as_c_ptr(), + serialized.len(), + ) + .into_vec() + }; + let signed: LegacyProto::SigningOutput = tw_proto::deserialize(&signed).unwrap(); + + // Check result. + assert_eq!(signed.error, CommonProto::SigningError::OK); + let encoded_hex = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(encoded_hex, "02000000013ab533f8709accfffd1de4fa29b6584ec78f5a2f23947c938f835a3e916305c5000000006b48304502210086ab2c2192e2738529d6cd9604d8ee75c5b09b0c2f4066a5c5fa3f87a26c0af602202afc7096aaa992235c43e712146057b5ed6a776d82b9129620bc5a21991c0a5301210351e003fdc48e7f31c9bc94996c91f6c3273b7ef4208a1686021bedf7673bb058ffffffff01c0aff62901000000225120e01cfdd05da8fa1d71f987373f3790d45dea9861acb0525c86656fe50f4397a600000000"); + + // Next transaction; try to spend the P2TR key-path. + + let txid: Vec = hex("9a582032f6a50cedaff77d3d5604b33adf8bc31bdaef8de977c2187e395860ac") + .into_iter() + .rev() + .collect(); + + // Output. + let output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2tr_key_path_script( + ONE_BTC_I64 * 50 - MINER_FEE_I64 - MINER_FEE_I64, + alice_pubkey.as_c_ptr(), + alice_pubkey.len(), + ) + .into_vec() + }; + let output: LegacyProto::TransactionOutput = tw_proto::deserialize(&output).unwrap(); + + // Prepare SigningInput. + let signing = LegacyProto::SigningInput { + private_key: vec![bob_private_key.into()], + utxo: vec![LegacyProto::UnspentTransaction { + out_point: Some(LegacyProto::OutPoint { + hash: txid.into(), + index: 0, + sequence: u32::MAX, + ..Default::default() + }), + // For inputs, script is not needed (derived from variant). + script: Default::default(), + amount: ONE_BTC_I64 * 50 - MINER_FEE_I64, + variant: LegacyProto::TransactionVariant::P2TRKEYPATH, + spendingScript: Default::default(), + }], + plan: Some(LegacyProto::TransactionPlan { + utxos: vec![LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: output.script, + amount: output.value, + variant: LegacyProto::TransactionVariant::P2TRKEYPATH, + spendingScript: Default::default(), + }], + ..Default::default() + }), + ..Default::default() + }; + let serialized = tw_proto::serialize(&signing).unwrap(); + + // Sign and build the transaction. + let signed = unsafe { + legacy_ffi::tw_bitcoin_legacy_taproot_build_and_sign_transaction( + serialized.as_c_ptr(), + serialized.len(), + ) + .into_vec() + }; + let signed: LegacyProto::SigningOutput = tw_proto::deserialize(&signed).unwrap(); + + // Check result. + const REVEAL_RAW: &str = "02000000000101ac6058397e18c277e98defda1bc38bdf3ab304563d7df7afed0ca5f63220589a0000000000ffffffff01806de72901000000225120a5c027857e359d19f625e52a106b8ac6ca2d6a8728f6cf2107cd7958ee0787c20140ec2d3910d41506b60aaa20520bb72f15e2d2cbd97e3a8e26ee7bad5f4c56b0f2fb0ceaddac33cb2813a33ba017ba6b1d011bab74a0426f12a2bcf47b4ed5bc8600000000"; + + assert_eq!(signed.error, CommonProto::SigningError::OK); + let encoded_hex = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(encoded_hex[..188], REVEAL_RAW[..188]); + // Schnorr signature does not match (non-deterministic). + assert_ne!(encoded_hex[188..316], REVEAL_RAW[188..316]); + assert_eq!(encoded_hex[316..], REVEAL_RAW[316..]); +} + +#[test] +fn ffi_proto_sign_input_p2wpkh_output_brc20() { + let alice_private_key = hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + let alice_pubkey = hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); + + let txid = hex("8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008") + .into_iter() + .rev() + .collect(); + + // Output. + let c_ticker = CString::new("oadf").unwrap(); + let brc20_output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_brc20_transfer_inscription( + c_ticker.as_ptr(), + 20, + 7_000, + alice_pubkey.as_c_ptr(), + alice_pubkey.len(), + ) + .into_vec() + }; + let brc20_output: LegacyProto::TransactionOutput = + tw_proto::deserialize(&brc20_output).unwrap(); + + // Change output. + let change_output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2wpkh_script( + 16_400, + alice_pubkey.as_c_ptr(), + alice_pubkey.len(), + ) + .into_vec() + }; + let change_output: LegacyProto::TransactionOutput = + tw_proto::deserialize(&change_output).unwrap(); + + // Prepare SigningInput. + let signing = LegacyProto::SigningInput { + private_key: vec![alice_private_key.clone().into()], + utxo: vec![LegacyProto::UnspentTransaction { + out_point: Some(LegacyProto::OutPoint { + hash: txid, + index: 1, + sequence: u32::MAX, + ..Default::default() + }), + // For inputs, script is not needed (derived from variant). + script: Default::default(), + amount: 26_400, + variant: LegacyProto::TransactionVariant::P2WPKH, + spendingScript: Default::default(), + }], + plan: Some(LegacyProto::TransactionPlan { + utxos: vec![ + LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: brc20_output.script.clone(), + amount: brc20_output.value, + variant: LegacyProto::TransactionVariant::BRC20TRANSFER, + spendingScript: Default::default(), + }, + // Change output. + LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: change_output.script, + amount: change_output.value, + variant: LegacyProto::TransactionVariant::P2WPKH, + spendingScript: Default::default(), + }, + ], + ..Default::default() + }), + ..Default::default() + }; + let serialized = tw_proto::serialize(&signing).unwrap(); + + // Sign and build the transaction. + let signed = unsafe { + legacy_ffi::tw_bitcoin_legacy_taproot_build_and_sign_transaction( + serialized.as_c_ptr(), + serialized.len(), + ) + .into_vec() + }; + let signed: LegacyProto::SigningOutput = tw_proto::deserialize(&signed).unwrap(); + + // Check result. + assert_eq!(signed.error, CommonProto::SigningError::OK); + let encoded_hex = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(encoded_hex, "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + + // Next transaction; try to spend the BRC20 transfer inscription. + + let txid: Vec = hex("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + .into_iter() + .rev() + .collect(); + + // Tagged output. + let output = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2wpkh_script( + 546, + alice_pubkey.as_c_ptr(), + alice_pubkey.len(), + ) + .into_vec() + }; + let output: LegacyProto::TransactionOutput = tw_proto::deserialize(&output).unwrap(); + + // Prepare SigningInput. + let signing = LegacyProto::SigningInput { + private_key: vec![alice_private_key.into()], + utxo: vec![LegacyProto::UnspentTransaction { + out_point: Some(LegacyProto::OutPoint { + hash: txid.into(), + index: 0, + sequence: u32::MAX, + ..Default::default() + }), + script: Default::default(), + amount: brc20_output.value, + variant: LegacyProto::TransactionVariant::BRC20TRANSFER, + // IMPORTANT: spending script is specified. + spendingScript: brc20_output.spendingScript, + }], + plan: Some(LegacyProto::TransactionPlan { + utxos: vec![LegacyProto::UnspentTransaction { + out_point: Default::default(), + script: output.script, + amount: 546, + variant: LegacyProto::TransactionVariant::P2WPKH, + spendingScript: Default::default(), + }], + ..Default::default() + }), + ..Default::default() + }; + let serialized = tw_proto::serialize(&signing).unwrap(); + + // Sign and build the transaction. + let signed = unsafe { + legacy_ffi::tw_bitcoin_legacy_taproot_build_and_sign_transaction( + serialized.as_c_ptr(), + serialized.len(), + ) + .into_vec() + }; + let signed: LegacyProto::SigningOutput = tw_proto::deserialize(&signed).unwrap(); + + // Check result. + const REVEAL_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03406a35548b8fa4620028e021a944c1d3dc6e947243a7bfc901bf63fefae0d2460efa149a6440cab51966aa4f09faef2d1e5efcba23ab4ca6e669da598022dbcfe35b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; + + assert_eq!(signed.error, CommonProto::SigningError::OK); + let encoded_hex = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(encoded_hex[..164], REVEAL_RAW[..164]); + // Schnorr signature does not match (non-deterministic). + assert_ne!(encoded_hex[164..292], REVEAL_RAW[164..292]); + assert_eq!(encoded_hex[292..], REVEAL_RAW[292..]); +} diff --git a/rust/tw_bitcoin/tests/legacy_scripts.rs b/rust/tw_bitcoin/tests/legacy_scripts.rs new file mode 100644 index 00000000000..77d5fadf82d --- /dev/null +++ b/rust/tw_bitcoin/tests/legacy_scripts.rs @@ -0,0 +1,177 @@ +#![allow(deprecated)] + +mod common; + +use bitcoin::{PublicKey, ScriptBuf}; +use secp256k1::XOnlyPublicKey; +use std::ffi::CString; +use tw_bitcoin::modules::transactions::{ + BRC20TransferInscription, Brc20Ticker, OrdinalNftInscription, +}; +use tw_encoding::hex; +use tw_proto::Bitcoin::Proto as LegacyProto; +use wallet_core_rs::ffi::bitcoin::legacy as legacy_ffi; + +// When building the spending conditions of inputs (scriptPubkey), then the +// actual value is not important. We can just use 0 here. +const SATOSHIS: i64 = 0; +const PUBKEY: &str = "028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"; + +#[test] +fn ffi_tw_bitcoin_legacy_build_p2pkh_script() { + let pubkey_slice = hex::decode(PUBKEY).unwrap(); + let pubkey = PublicKey::from_slice(&pubkey_slice).unwrap(); + + let raw = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2pkh_script( + SATOSHIS, + pubkey_slice.as_ptr(), + pubkey_slice.len(), + ) + .into_vec() + }; + + // The expected script. + let expected = ScriptBuf::new_p2pkh(&pubkey.pubkey_hash()); + + let proto: LegacyProto::TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + assert_eq!(proto.value, SATOSHIS); + assert_eq!(proto.script, expected.as_bytes()); + assert!(proto.spendingScript.is_empty()); +} + +#[test] +fn ffi_tw_bitcoin_legacy_build_p2wpkh_script() { + let pubkey_slice = hex::decode(PUBKEY).unwrap(); + let pubkey = PublicKey::from_slice(&pubkey_slice).unwrap(); + + let raw = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2wpkh_script( + SATOSHIS, + pubkey_slice.as_ptr(), + pubkey_slice.len(), + ) + .into_vec() + }; + + // The expected script. + let expected = ScriptBuf::new_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()); + + let proto: LegacyProto::TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + assert_eq!(proto.value, SATOSHIS); + assert_eq!(proto.script, expected.as_bytes()); + assert!(proto.spendingScript.is_empty()); +} + +#[test] +fn ffi_tw_bitcoin_legacy_build_p2tr_key_path_script() { + let pubkey_slice = hex::decode(PUBKEY).unwrap(); + let pubkey = PublicKey::from_slice(&pubkey_slice).unwrap(); + + let raw = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_p2tr_key_path_script( + SATOSHIS, + pubkey_slice.as_ptr(), + pubkey_slice.len(), + ) + .into_vec() + }; + + // The expected script. + let xonly = XOnlyPublicKey::from(pubkey.inner); + let expected = ScriptBuf::new_v1_p2tr(&secp256k1::Secp256k1::new(), xonly, None); + + let proto: LegacyProto::TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + assert_eq!(proto.value, SATOSHIS); + assert_eq!(proto.script, expected.as_bytes()); + assert!(proto.spendingScript.is_empty()); +} + +#[test] +fn ffi_tw_bitcoin_legacy_build_brc20_transfer_inscription() { + let pubkey_slice = hex::decode(PUBKEY).unwrap(); + let pubkey = PublicKey::from_slice(&pubkey_slice).unwrap(); + + let ticker_str = "oadf"; + let c_ticker = CString::new(ticker_str).unwrap(); + let brc20_amount = 100; + + // Call the FFI function. + let raw = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_brc20_transfer_inscription( + c_ticker.as_ptr(), + brc20_amount, + SATOSHIS, + pubkey_slice.as_ptr(), + pubkey_slice.len(), + ) + .into_vec() + }; + + // Prepare the BRC20 payload + merkle root. + let ticker = Brc20Ticker::new(ticker_str.to_string()).unwrap(); + let transfer = BRC20TransferInscription::new(pubkey, ticker, brc20_amount).unwrap(); + + let merkle_root = transfer + .inscription() + .spend_info() + .merkle_root() + .expect("incorrectly constructed Taproot merkle root"); + + // The expected script. + let xonly = XOnlyPublicKey::from(pubkey.inner); + let expected = ScriptBuf::new_v1_p2tr(&secp256k1::Secp256k1::new(), xonly, Some(merkle_root)); + + let proto: LegacyProto::TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + assert_eq!(proto.value, SATOSHIS); + assert_eq!(proto.script, expected.as_bytes()); + assert_eq!( + proto.spendingScript, + transfer.inscription().taproot_program().as_bytes() + ); +} + +#[test] +fn ffi_tw_bitcoin_legacy_build_nft_inscription() { + let pubkey_slice = hex::decode(PUBKEY).unwrap(); + let pubkey = PublicKey::from_slice(&pubkey_slice).unwrap(); + + let mime_type = "image/png"; + let c_mime_type = CString::new(mime_type).unwrap(); + let payload_hex = common::data::NFT_INSCRIPTION_IMAGE_DATA; + let payload = tw_encoding::hex::decode(payload_hex).unwrap(); + + // Call the FFI function. + let raw = unsafe { + legacy_ffi::tw_bitcoin_legacy_build_nft_inscription( + c_mime_type.as_ptr(), + payload.as_ptr(), + payload.len(), + SATOSHIS, + pubkey_slice.as_ptr(), + pubkey_slice.len(), + ) + .into_vec() + }; + + // Prepare the NFT inscription + merkle root. + let nft = OrdinalNftInscription::new(mime_type.as_bytes(), &payload, pubkey).unwrap(); + + let merkle_root = nft + .inscription() + .spend_info() + .merkle_root() + .expect("incorrectly constructed Taproot merkle root"); + + // The expected script. + let xonly = XOnlyPublicKey::from(pubkey.inner); + let expected = ScriptBuf::new_v1_p2tr(&secp256k1::Secp256k1::new(), xonly, Some(merkle_root)); + + let proto: LegacyProto::TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + assert_eq!(proto.value, SATOSHIS); + assert_eq!(proto.script, expected.as_bytes()); + assert_eq!( + proto.spendingScript, + nft.inscription().taproot_program().as_bytes() + ); +} diff --git a/rust/tw_bitcoin/tests/ordinal_nft.rs b/rust/tw_bitcoin/tests/ordinal_nft.rs new file mode 100644 index 00000000000..23ff1694a04 --- /dev/null +++ b/rust/tw_bitcoin/tests/ordinal_nft.rs @@ -0,0 +1,130 @@ +mod common; + +use common::hex; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_sign_ordinal_nft_commit_reveal_transfer() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + let alice_pubkey = hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); + + let txid: Vec = hex("579590c3227253ad423b1e7e3c5b073b8a280d307c68aecd779df2600daa2f99") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 32_400, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 31_100, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::ordinal_inscribe( + Proto::mod_Output::OutputOrdinalInscription { + inscribe_to: alice_pubkey.as_slice().into(), + mime_type: "image/png".into(), + payload: hex(common::data::NFT_INSCRIPTION_IMAGE_DATA).into(), + }, + ), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + // https://www.blockchain.com/explorer/transactions/btc/f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117 + assert_eq!( + signed.txid, + hex("f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117") + ); + + let encoded = tw_encoding::hex::encode(signed.encoded, false); + let transaction = signed.transaction.unwrap(); + + assert_eq!(transaction.inputs.len(), 1); + assert_eq!(transaction.outputs.len(), 1); + assert_eq!(&encoded, "02000000000101992faa0d60f29d77cdae687c300d288a3b075b3c7e1e3b42ad537222c39095570000000000ffffffff017c790000000000002251202ac69a7e9dba801e9fcba826055917b84ca6fba4d51a29e47d478de603eedab602473044022054212984443ed4c66fc103d825bfd2da7baf2ab65d286e3c629b36b98cd7debd022050214cfe5d3b12a17aaaf1a196bfeb2f0ad15ffb320c4717eb7614162453e4fe0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + + let txid: Vec = hex("f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 31_100, + sighash_type: UtxoProto::SighashType::UseDefault, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::ordinal_inscribe( + Proto::mod_Input::InputOrdinalInscription { + one_prevout: false, + inscribe_to: alice_pubkey.as_slice().into(), + mime_type: "image/png".into(), + payload: hex(common::data::NFT_INSCRIPTION_IMAGE_DATA).into(), + }, + ), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 546, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + // We enable deterministic Schnorr signatures here + dangerous_use_fixed_schnorr_rng: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + // https://www.blockchain.com/explorer/transactions/btc/173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac + assert_eq!( + signed.txid, + hex("173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac") + ); + + let encoded = tw_encoding::hex::encode(signed.encoded, false); + let transaction = signed.transaction.unwrap(); + + assert_eq!(encoded, common::data::NFT_INSCRIPTION_RAW_HEX); + assert_eq!(transaction.inputs.len(), 1); + assert_eq!(transaction.outputs.len(), 1); +} diff --git a/rust/tw_bitcoin/tests/p2pkh.rs b/rust/tw_bitcoin/tests/p2pkh.rs new file mode 100644 index 00000000000..05f99c8d438 --- /dev/null +++ b/rust/tw_bitcoin/tests/p2pkh.rs @@ -0,0 +1,75 @@ +mod common; + +use common::{hex, MINER_FEE, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_emtpy() { + let _coin = EmptyCoinContext; + let alice_private_key = hex("56429688a1a6b00b90ccd22a0de0a376b6569d8684022ae92229a28478bfb657"); + + let signing = Proto::SigningInput { + private_key: alice_private_key.into(), + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&_coin, signing); + assert_eq!(signed.error, Proto::Error::OK); +} + +#[test] +fn coin_entry_sign_input_p2pkh_output_p2pkh() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("56429688a1a6b00b90ccd22a0de0a376b6569d8684022ae92229a28478bfb657"); + let alice_pubkey = hex("036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536"); + let _bob_private_key = hex("b7da1ec42b19085fe09fec54b9d9eacd998ae4e6d2ad472be38d8393391b9ead"); + let bob_pubkey = hex("037ed9a436e11ec4947ac4b7823787e24ba73180f1edd2857bff19c9f4d62b65bf"); + + // Create transaction with P2PKH as input and output. + let txid: Vec = hex("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 50, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC * 50 - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000"); +} diff --git a/rust/tw_bitcoin/tests/p2sh.rs b/rust/tw_bitcoin/tests/p2sh.rs new file mode 100644 index 00000000000..d09a15165ad --- /dev/null +++ b/rust/tw_bitcoin/tests/p2sh.rs @@ -0,0 +1,147 @@ +mod common; + +use bitcoin::script::PushBytesBuf; +use bitcoin::{PublicKey, ScriptBuf}; +use common::{hex, MINER_FEE, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_bitcoin::modules::signer::Signer; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_sign_input_p2pkh_output_p2sh() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("56429688a1a6b00b90ccd22a0de0a376b6569d8684022ae92229a28478bfb657"); + let alice_pubkey = hex("036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536"); + let bob_private_key = hex("b7da1ec42b19085fe09fec54b9d9eacd998ae4e6d2ad472be38d8393391b9ead"); + let bob_pubkey = hex("037ed9a436e11ec4947ac4b7823787e24ba73180f1edd2857bff19c9f4d62b65bf"); + + // Create transaction with P2SH as output (spend). + + let txid: Vec = hex("e7503721268d0547b3b009dab56e5ebd8bcadbfc7dfae3468a56b5cb0c07a2f7") + .into_iter() + .rev() + .collect(); + + // We use a simple P2PKH as the redeem script (ie. P2PKH embedded inside P2SH). + let bob_native_pubkey = PublicKey::from_slice(&bob_pubkey).unwrap(); + let redeem_script = ScriptBuf::new_p2pkh(&bob_native_pubkey.pubkey_hash()); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 50 * ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 50 * ONE_BTC - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2sh(Proto::mod_Output::OutputRedeemScriptOrHash { + variant: ProtoOutputRedeemScriptOrHashBuilder::redeem_script( + redeem_script.as_bytes().into(), + ), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "0200000001f7a2070ccbb5568a46e3fa7dfcdbca8bbd5e6eb5da09b0b347058d26213750e7000000006a473044022007c88caf624c0a130fc79d2835ed5b6db49f2dea0d5e685f06138aaa4a904d690220243fe7744c8b48759e74a87075de3f548988252a770871fc1444652bb32ec46e0121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff6290100000017a914a519b524d55ae8972e8e0e6b9d645ab20eb2635e8700000000"); + + // Create transaction with P2SH as input (claim). + + let txid: Vec = hex("5d99b77a411a879fb6fa5b442f0d121965346d8e5ab61e0d189967fd5f49bd82") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 50 * ONE_BTC - MINER_FEE, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + // The way P2SH is signed in Bitcoin, we first place the redeem script directly here. + variant: ProtoInputBuilder::p2sh(redeem_script.as_bytes().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 50 * ONE_BTC - MINER_FEE - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let mut signing = Proto::SigningInput { + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + // Generate the sighashes. + let sighashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); + assert_eq!(sighashes.error, Proto::Error::OK); + + // Sign the sighashes. + let signatures = Signer::signatures_from_proto( + &sighashes, + bob_private_key.to_vec(), + Default::default(), + false, + ) + .unwrap(); + + let sig = &signatures[0]; + + // Construc the final redeem scrip with the necessary stack items (signature + pubkey). + let mut sig_buf = PushBytesBuf::new(); + sig_buf.extend_from_slice(sig).unwrap(); + + let mut redeem_buf = PushBytesBuf::new(); + redeem_buf + .extend_from_slice(redeem_script.as_bytes()) + .unwrap(); + + let finalized = ScriptBuf::builder() + .push_slice(sig_buf) + .push_key(&bob_native_pubkey) + .push_slice(redeem_buf.as_push_bytes()) + .into_script(); + + // Now that we've signed the input, we update the input with the complete, + // finalized redeem script. + signing.inputs[0].to_recipient = ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2sh(finalized.as_bytes().into()), + }); + + // Compile the final transaction. + let signed = BitcoinEntry.compile(&coin, signing, signatures, vec![]); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "020000000182bd495ffd6799180d1eb65a8e6d346519120d2f445bfab69f871a417ab7995d000000008447304402207aad4b72c6d78c81a1e795325bd5ddb449f0a1363205903f5e37950e6b89054102202aaf4dd919700d21fe2431352df99c434378bd0d46b778b445079579300effdf0121037ed9a436e11ec4947ac4b7823787e24ba73180f1edd2857bff19c9f4d62b65bf1976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88acffffffff01806de729010000001976a914e4c1ea86373d554b8f4efff2cfb0001ea19124d288ac00000000"); +} diff --git a/rust/tw_bitcoin/tests/p2tr_key_path.rs b/rust/tw_bitcoin/tests/p2tr_key_path.rs new file mode 100644 index 00000000000..c32c230c396 --- /dev/null +++ b/rust/tw_bitcoin/tests/p2tr_key_path.rs @@ -0,0 +1,98 @@ +mod common; + +use common::{hex, MINER_FEE, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_sign_input_p2pkh_output_p2tr_key_path() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("12ce558df23528f1aa86f1f51ac7e13a197a06bda27610fa89e13b04c40ee999"); + let alice_pubkey = hex("0351e003fdc48e7f31c9bc94996c91f6c3273b7ef4208a1686021bedf7673bb058"); + let bob_private_key = hex("26c2566adcc030a1799213bfd546e615f6ab06f72085ec6806ff1761da48d227"); + let bob_pubkey = hex("02c0938cf377023dfde55e9c96b3cff4ca8894fb6b5d2009006bd43c0bff69cac9"); + + let txid: Vec = hex("c50563913e5a838f937c94232f5a8fc74e58b629fae41dfdffcc9a70f833b53a") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 50, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC * 50 - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2tr_key_path(bob_pubkey.as_slice().into()), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "02000000013ab533f8709accfffd1de4fa29b6584ec78f5a2f23947c938f835a3e916305c5000000006b48304502210086ab2c2192e2738529d6cd9604d8ee75c5b09b0c2f4066a5c5fa3f87a26c0af602202afc7096aaa992235c43e712146057b5ed6a776d82b9129620bc5a21991c0a5301210351e003fdc48e7f31c9bc94996c91f6c3273b7ef4208a1686021bedf7673bb058ffffffff01c0aff62901000000225120e01cfdd05da8fa1d71f987373f3790d45dea9861acb0525c86656fe50f4397a600000000"); + + let txid: Vec = hex("9a582032f6a50cedaff77d3d5604b33adf8bc31bdaef8de977c2187e395860ac") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 50 - MINER_FEE, + sighash_type: UtxoProto::SighashType::UseDefault, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2tr_key_path(Proto::mod_Input::InputTaprootKeyPath { + public_key: bob_pubkey.as_slice().into(), + one_prevout: false, + }), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC * 50 - MINER_FEE - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2tr_key_path(alice_pubkey.as_slice().into()), + }), + }; + + let signing = Proto::SigningInput { + private_key: bob_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + // We enable deterministic Schnorr signatures here + dangerous_use_fixed_schnorr_rng: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "02000000000101ac6058397e18c277e98defda1bc38bdf3ab304563d7df7afed0ca5f63220589a0000000000ffffffff01806de72901000000225120a5c027857e359d19f625e52a106b8ac6ca2d6a8728f6cf2107cd7958ee0787c20140ec2d3910d41506b60aaa20520bb72f15e2d2cbd97e3a8e26ee7bad5f4c56b0f2fb0ceaddac33cb2813a33ba017ba6b1d011bab74a0426f12a2bcf47b4ed5bc8600000000"); +} diff --git a/rust/tw_bitcoin/tests/p2tr_script_path.rs b/rust/tw_bitcoin/tests/p2tr_script_path.rs new file mode 100644 index 00000000000..b2f01b8f606 --- /dev/null +++ b/rust/tw_bitcoin/tests/p2tr_script_path.rs @@ -0,0 +1,161 @@ +mod common; + +use bitcoin::taproot::LeafVersion; +use bitcoin::PublicKey; +use common::hex; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_bitcoin::modules::transactions::{BRC20TransferInscription, Brc20Ticker}; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +/// A test for the custom P2TR script-path builders. This test essentially +/// reconstruct the BRC20 transfer tests, but without using the convenience +/// builders. +fn coin_entry_custom_script_path() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + let alice_pubkey = hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); + + let txid: Vec = hex("8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 1, + value: 26_400, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + // Build the BRC20 transfer outside the library, only provide essential + // information to the builder. + let ticker = Brc20Ticker::new("oadf".to_string()).unwrap(); + let inscribe_to = PublicKey::from_slice(&alice_pubkey).unwrap(); + let transfer = BRC20TransferInscription::new(inscribe_to, ticker, 20).unwrap(); + let merkle_root = transfer.inscription().spend_info().merkle_root().unwrap(); + + // Provide the public key ("internal key") and the merkle root directly to the builder. + let out1 = Proto::Output { + value: 7_000, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2tr_script_path( + Proto::mod_Output::OutputTaprootScriptPath { + internal_key: alice_pubkey.to_vec().into(), + merkle_root: merkle_root.to_vec().into(), + }, + ), + }), + }; + + // Change/return transaction. + let out2 = Proto::Output { + value: 16_400, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1, out2], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!( + signed.txid, + hex("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + ); + + let encoded = tw_encoding::hex::encode(signed.encoded, false); + let transaction = signed.transaction.unwrap(); + + assert_eq!(transaction.inputs.len(), 1); + assert_eq!(transaction.outputs.len(), 2); + assert_eq!(&encoded, "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + + // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 + let txid: Vec = hex("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + .into_iter() + .rev() + .collect(); + + // Prepare the BRC20 payload and control block. + let payload = transfer.inscription().taproot_program().to_owned(); + let control_block = transfer + .inscription() + .spend_info() + .control_block(&(payload.to_owned(), LeafVersion::TapScript)) + .unwrap(); + + // Provide the the payload and control block directly to the builder. + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 7_000, + sighash_type: UtxoProto::SighashType::UseDefault, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2tr_script_path( + Proto::mod_Input::InputTaprootScriptPath { + one_prevout: false, + payload: payload.to_vec().into(), + control_block: control_block.serialize().into(), + }, + ), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 546, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + // We enable deterministic Schnorr signatures here + dangerous_use_fixed_schnorr_rng: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + // https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca + assert_eq!( + signed.txid, + hex("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") + ); + + let encoded = tw_encoding::hex::encode(signed.encoded, false); + let transaction = signed.transaction.unwrap(); + + assert_eq!(encoded, "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03406a35548b8fa4620028e021a944c1d3dc6e947243a7bfc901bf63fefae0d2460efa149a6440cab51966aa4f09faef2d1e5efcba23ab4ca6e669da598022dbcfe35b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + assert_eq!(transaction.inputs.len(), 1); + assert_eq!(transaction.outputs.len(), 1); +} diff --git a/rust/tw_bitcoin/tests/p2wpkh.rs b/rust/tw_bitcoin/tests/p2wpkh.rs new file mode 100644 index 00000000000..5dfedc04959 --- /dev/null +++ b/rust/tw_bitcoin/tests/p2wpkh.rs @@ -0,0 +1,102 @@ +mod common; + +use common::{hex, MINER_FEE, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_sign_input_p2pkh_output_p2wpkh() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + let bob_private_key = hex("05dead4689ec7d55de654771120866be83bf1b8e25c9a1b77fc58a336e1cd1a3"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + // Create transaction with P2WPKH as output. + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 50, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC * 50 - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000"); + + // Create transaction with P2WPKH as input (claim). + + let txid: Vec = hex("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 50 - MINER_FEE, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(bob_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC * 50 - MINER_FEE - MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: bob_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "020000000001016e1f16dcfafbb3a83697f6c23c624cd71085a7f8a25ce0bd9743a41d0a458e850000000000ffffffff01806de7290100000016001460cda7b50f14c152d7401c28ae773c698db9237302483045022100a9b517de5a5e036d7133df499b5b751db6f9a01576a6c5dc38229ec08b6c45cd02200e42c9f8c707c9bf0ceab4f739ec8d683dc1f1f29e195a8da9bc183584d624a60121025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f00000000"); +} diff --git a/rust/tw_bitcoin/tests/p2wsh.rs b/rust/tw_bitcoin/tests/p2wsh.rs new file mode 100644 index 00000000000..fd50fd831d5 --- /dev/null +++ b/rust/tw_bitcoin/tests/p2wsh.rs @@ -0,0 +1,142 @@ +mod common; + +use bitcoin::consensus::Encodable; +use bitcoin::{PublicKey, ScriptBuf, Witness}; +use common::{hex, MINER_FEE, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_bitcoin::modules::signer::Signer; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn coin_entry_sign_input_p2pkh_output_p2wsh() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("56429688a1a6b00b90ccd22a0de0a376b6569d8684022ae92229a28478bfb657"); + let alice_pubkey = hex("036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536"); + let bob_private_key = hex("b7da1ec42b19085fe09fec54b9d9eacd998ae4e6d2ad472be38d8393391b9ead"); + let bob_pubkey = hex("037ed9a436e11ec4947ac4b7823787e24ba73180f1edd2857bff19c9f4d62b65bf"); + + // Create transaction with P2WSH as output (spend). + + let txid: Vec = hex("c01007bb55bde4e70278e1154c34db72f34a833687d3f37443bd5c49137ee5fe") + .into_iter() + .rev() + .collect(); + + // We use a simple P2PKH as the redeem script (ie. P2PKH embedded inside P2WSH). + let bob_native_pubkey = PublicKey::from_slice(&bob_pubkey).unwrap(); + let redeem_script = ScriptBuf::new_p2pkh(&bob_native_pubkey.pubkey_hash()); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 50 * ONE_BTC - 2 * MINER_FEE, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 50 * ONE_BTC - 3 * MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wsh(Proto::mod_Output::OutputRedeemScriptOrHash { + variant: ProtoOutputRedeemScriptOrHashBuilder::redeem_script( + redeem_script.as_bytes().into(), + ), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + change_output: Default::default(), + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "0200000001fee57e13495cbd4374f3d38736834af372db344c15e17802e7e4bd55bb0710c0000000006b483045022100edd19b379131b9f7a05ff2a79313ccec181f2e3fd06901e27ffff4fa500dbf95022060ae976cd70b8af956fdf4582bb1289bdf233a5bace82e69ea9b7cfb55c583370121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01402bd82901000000220020883a539555e537e0498732376a3d4d282e304bce7bfda6876a2b63b08a04f54400000000"); + + // Create transaction with P2WSH as input (claim). + + let txid: Vec = hex("dd9d4ca23532f5c89d016e1aacef1210ab5b9d00527c633969841daca7dd17c7") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 50 * ONE_BTC - 3 * MINER_FEE, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + // The way P2WSH is signed in Bitcoin, we first place the redeem script directly here. + variant: ProtoInputBuilder::p2wsh(redeem_script.to_bytes().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 50 * ONE_BTC - 4 * MINER_FEE, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let mut signing = Proto::SigningInput { + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + // Generate the sighashes. + let sighashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); + + // Sign the sighashes. + let signatures = Signer::signatures_from_proto( + &sighashes, + bob_private_key.to_vec(), + Default::default(), + false, + ) + .unwrap(); + + let sig = &signatures[0]; + + // Construc the final redeem witness stack with the necessary witness stack items (signature + pubkey). + let mut finalized = Witness::new(); + finalized.push(sig); + finalized.push(bob_native_pubkey.to_bytes()); + finalized.push(redeem_script); + + // The witness stack must be encoded correctly. + let mut encoded = vec![]; + let _ = finalized.consensus_encode(&mut encoded).unwrap(); + + // Now that we've signed the input, we update the input with the complete, + // finalized redeem witness stack.. + signing.inputs[0].to_recipient = ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wsh(encoded.into()), + }); + + // Compile the final transaction. + let signed = BitcoinEntry.compile(&coin, signing, signatures, vec![]); + let encoded = tw_encoding::hex::encode(signed.encoded, false); + assert_eq!(signed.error, Proto::Error::OK); + assert_eq!(&encoded, "02000000000101c717dda7ac1d846939637c52009d5bab1012efac1a6e019dc8f53235a24c9ddd0000000000ffffffff0100e9c829010000001976a914e4c1ea86373d554b8f4efff2cfb0001ea19124d288ac0347304402201d22810b5580a49a2e73d7c4ea90754b5d70d36adb9a8f0c9cb7393da1d1d28f02207683b2e3d31a5c7e74126681f1f2a7249b7a3a918d5890ef69b94bd3bb4fb9300121037ed9a436e11ec4947ac4b7823787e24ba73180f1edd2857bff19c9f4d62b65bf1976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000"); +} diff --git a/rust/tw_bitcoin/tests/plan_builder.rs b/rust/tw_bitcoin/tests/plan_builder.rs new file mode 100644 index 00000000000..935c5ada511 --- /dev/null +++ b/rust/tw_bitcoin/tests/plan_builder.rs @@ -0,0 +1,192 @@ +mod common; + +use common::{hex, ONE_BTC}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::modules::plan_builder::PlanBuilder; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn transaction_plan_compose_brc20() { + let _coin = EmptyCoinContext; + + let alice_private_key = hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + let alice_pubkey = hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); + + let txid1: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid1.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid2: Vec = hex("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e") + .into_iter() + .rev() + .collect(); + + let tx2 = Proto::Input { + txid: txid2.as_slice().into(), + vout: 0, + value: ONE_BTC * 2, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let tagged_output = Proto::Output { + value: 546, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let change_output = Proto::Output { + // Will be set by the library. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let brc20_inscription = Proto::mod_Input::InputBrc20Inscription { + one_prevout: false, + inscribe_to: alice_pubkey.as_slice().into(), + ticker: "oadf".into(), + transfer_amount: 20, + }; + + let compose = Proto::ComposePlan { + compose: Proto::mod_ComposePlan::OneOfcompose::brc20( + Proto::mod_ComposePlan::ComposeBrc20Plan { + private_key: alice_private_key.clone().into(), + inputs: vec![tx1.clone(), tx2.clone()], + input_selector: UtxoProto::InputSelector::SelectAscending, + tagged_output: Some(tagged_output.clone()), + inscription: Some(brc20_inscription), + fee_per_vb: 25, + change_output: Some(change_output.clone()), + disable_change_output: false, + }, + ), + }; + + // Compute plan + let builder = BitcoinEntry.plan_builder().unwrap(); + let built = builder.plan(&_coin, compose); + assert_eq!(built.error, Proto::Error::OK); + + let Proto::mod_TransactionPlan::OneOfplan::brc20(plan) = built.plan else { panic!() }; + + // Check basics of the COMMIT transaction. + let commit_signing = { + let mut commit = plan.commit.unwrap(); + // One input covers all outputs. + assert_eq!(commit.version, 2); + assert_eq!(commit.private_key, alice_private_key); + assert_eq!(commit.inputs.len(), 1); + // BRC20 inscription output + change. + assert_eq!(commit.outputs.len(), 2); + // Use inputs as provided (already selected by TransactionPlan). + assert_eq!(commit.input_selector, UtxoProto::InputSelector::UseAll); + assert_eq!(commit.fee_per_vb, 0); + // Change output generation is disabled, inclulded in `commit.outputs`. + assert_eq!(commit.change_output, Default::default()); + assert!(commit.disable_change_output); + + // Check first input. + assert_eq!(commit.inputs[0], tx1); + + // Check first output. + let res_out_brc20 = &commit.outputs[0]; + assert_eq!(res_out_brc20.value, 3846); + let Proto::mod_Output::OneOfto_recipient::builder(builder) = &res_out_brc20.to_recipient else { panic!() }; + let Proto::mod_Output::mod_OutputBuilder::OneOfvariant::brc20_inscribe(brc20) = &builder.variant else { panic!() }; + assert_eq!(brc20.inscribe_to, alice_pubkey); + assert_eq!(brc20.ticker, "oadf"); + assert_eq!(brc20.transfer_amount, 20); + + // Check second output (ie. change output). + let res_out_change = &commit.outputs[1]; + assert_eq!(res_out_change.value, ONE_BTC - 3846 - 3175); // Change: tx1 value - out1 value + assert_eq!(res_out_change.to_recipient, change_output.to_recipient); + + commit.private_key = alice_private_key.clone().into(); + commit + }; + + // Check basics of the REVEAL transaction. + let reveal_signing = { + let mut reveal = plan.reveal.unwrap(); + assert_eq!(reveal.version, 2); + assert_eq!(reveal.private_key, alice_private_key); + // One inputs covers all outputs. + assert_eq!(reveal.inputs.len(), 1); + assert_eq!(reveal.outputs.len(), 1); + // Use inputs as provided. + assert_eq!(reveal.input_selector, UtxoProto::InputSelector::UseAll); + assert_eq!(reveal.fee_per_vb, 0); + // Change output generation is disabled. + assert_eq!(reveal.change_output, Default::default()); + assert!(reveal.disable_change_output); + + // Check first and only input. + let res_in_brc20 = &reveal.inputs[0]; + //assert_eq!(plan_input.txid, ) + assert_eq!(res_in_brc20.sequence, u32::MAX); + assert_eq!(res_in_brc20.value, 3846); + assert_eq!( + res_in_brc20.sighash_type, + UtxoProto::SighashType::UseDefault + ); + let Proto::mod_Input::OneOfto_recipient::builder(builder) = &res_in_brc20.to_recipient else { panic!() }; + let Proto::mod_Input::mod_InputBuilder::OneOfvariant::brc20_inscribe(brc20) = &builder.variant else { panic!() }; + assert_eq!(brc20.inscribe_to, alice_pubkey); + assert_eq!(brc20.ticker, "oadf"); + assert_eq!(brc20.transfer_amount, 20); + + // Check first and only output. + assert_eq!(reveal.outputs[0], tagged_output); + + reveal.private_key = alice_private_key.into(); + reveal + }; + + // Signed both transactions from the returned plan. + let commit_signed = BitcoinEntry.sign(&_coin, commit_signing); + assert_eq!(commit_signed.error, Proto::Error::OK); + let reveal_signed = BitcoinEntry.sign(&_coin, reveal_signing); + assert_eq!(reveal_signed.error, Proto::Error::OK); + + // Note that the API returns this in a non-reversed manner, so we need to reverse it first. + let commit_txid = commit_signed.txid.iter().copied().rev().collect::>(); + + // IMPORTANT: The input of the REVEAL transaction must reference the COMMIT transaction (Id). + assert_eq!( + commit_txid, + reveal_signed.transaction.as_ref().unwrap().inputs[0] + .txid + .as_ref() + ); + + //dbg!(&commit_signed); + //dbg!(&reveal_signed); +} diff --git a/rust/tw_bitcoin/tests/send_to_address.rs b/rust/tw_bitcoin/tests/send_to_address.rs new file mode 100644 index 00000000000..030148ea415 --- /dev/null +++ b/rust/tw_bitcoin/tests/send_to_address.rs @@ -0,0 +1,316 @@ +mod common; + +use bitcoin::{Address, PublicKey, ScriptBuf}; +use common::hex; +use secp256k1::XOnlyPublicKey; +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::empty_context::EmptyCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +#[test] +fn send_to_p2sh_address() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 10_000, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + // Create the P2SH address. + let recipient = PublicKey::from_slice(&bob_pubkey).unwrap(); + // We use a simple P2PKH as the redeem script. + let redeem_script = ScriptBuf::new_p2pkh(&recipient.pubkey_hash()); + let address = Address::p2sh(&redeem_script, bitcoin::Network::Bitcoin).unwrap(); + + // The output variant is derived from the specified address. + let out1 = Proto::Output { + value: 1_000, + to_recipient: ProtoOutputRecipient::from_address(address.to_string().into()), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + let tx = signed.transaction.as_ref().unwrap(); + assert_eq!(tx.inputs.len(), 1); + assert_eq!(tx.outputs.len(), 1); + + // The expected P2SH scriptPubkey + let expected = ScriptBuf::new_p2sh(&redeem_script.script_hash()); + + assert_eq!(tx.outputs[0].value, 1_000); + assert_eq!(tx.outputs[0].script_pubkey, expected.as_bytes()); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); +} + +#[test] +fn send_to_p2pkh_address() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 10_000, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + // Create the P2PKH address. + let recipient = PublicKey::from_slice(&bob_pubkey).unwrap(); + let address = Address::p2pkh(&recipient, bitcoin::Network::Bitcoin); + let address_string = address.to_string(); + + // The output variant is derived from the specified address. + let out1 = Proto::Output { + value: 1_000, + to_recipient: ProtoOutputRecipient::from_address(address_string.as_str().into()), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + let tx = signed.transaction.as_ref().unwrap(); + assert_eq!(tx.inputs.len(), 1); + assert_eq!(tx.outputs.len(), 1); + + // The expected P2PKH scriptPubkey + let expected = ScriptBuf::new_p2pkh(&recipient.pubkey_hash()); + + assert_eq!(tx.outputs[0].value, 1_000); + assert_eq!(tx.outputs[0].script_pubkey, expected.as_bytes()); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); +} + +#[test] +fn send_to_p2wsh_address() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 10_000, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + // Create the P2WSH address. + let recipient = PublicKey::from_slice(&bob_pubkey).unwrap(); + // We use a simple P2PKH as the redeem script. + let redeem_script = ScriptBuf::new_p2pkh(&recipient.pubkey_hash()); + let address = Address::p2wsh(&redeem_script, bitcoin::Network::Bitcoin); + + // The output variant is derived from the specified address. + let out1 = Proto::Output { + value: 1_000, + to_recipient: ProtoOutputRecipient::from_address(address.to_string().into()), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + let tx = signed.transaction.as_ref().unwrap(); + assert_eq!(tx.inputs.len(), 1); + assert_eq!(tx.outputs.len(), 1); + + // The expected Pw2SH scriptPubkey + let expected = ScriptBuf::new_v0_p2wsh(&redeem_script.wscript_hash()); + + assert_eq!(tx.outputs[0].value, 1_000); + assert_eq!(tx.outputs[0].script_pubkey, expected.as_bytes()); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); +} + +#[test] +fn send_to_p2wpkh_address() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 10_000, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + // Create the P2WPKH address. + let recipient = PublicKey::from_slice(&bob_pubkey).unwrap(); + let address = Address::p2wpkh(&recipient, bitcoin::Network::Bitcoin).unwrap(); + let address_string = address.to_string(); + + // The output variant is derived from the specified address. + let out1 = Proto::Output { + value: 1_000, + to_recipient: ProtoOutputRecipient::from_address(address_string.as_str().into()), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + let tx = signed.transaction.as_ref().unwrap(); + assert_eq!(tx.inputs.len(), 1); + assert_eq!(tx.outputs.len(), 1); + + // The expected P2WPKH scriptPubkey + let expected = ScriptBuf::new_v0_p2wpkh(&recipient.wpubkey_hash().unwrap()); + + assert_eq!(tx.outputs[0].value, 1_000); + assert_eq!(tx.outputs[0].script_pubkey, expected.as_bytes()); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); +} + +#[test] +fn send_to_p2tr_key_path_address() { + let coin = EmptyCoinContext; + + let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); + let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); + let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); + + let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") + .into_iter() + .rev() + .collect(); + + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: 10_000, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + // Create the P2TR key-path address. + let secp = secp256k1::Secp256k1::new(); + + let recipient = PublicKey::from_slice(&bob_pubkey).unwrap(); + let xonly = XOnlyPublicKey::from(recipient.inner); + let address = Address::p2tr(&secp, xonly, None, bitcoin::Network::Bitcoin); + let address_string = address.to_string(); + + // The output variant is derived from the specified address. + let out1 = Proto::Output { + value: 1_000, + to_recipient: ProtoOutputRecipient::from_address(address_string.as_str().into()), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + input_selector: UtxoProto::InputSelector::UseAll, + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + assert_eq!(signed.error, Proto::Error::OK); + + let tx = signed.transaction.as_ref().unwrap(); + assert_eq!(tx.inputs.len(), 1); + assert_eq!(tx.outputs.len(), 1); + + // The expected P2TR key-path scriptPubkey + let expected = ScriptBuf::new_v1_p2tr(&secp, xonly, None); + + assert_eq!(tx.outputs[0].value, 1_000); + assert_eq!(tx.outputs[0].script_pubkey, expected.as_bytes()); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); +} diff --git a/rust/tw_coin_entry/src/coin_entry.rs b/rust/tw_coin_entry/src/coin_entry.rs index 5379fe73212..b5727dfb698 100644 --- a/rust/tw_coin_entry/src/coin_entry.rs +++ b/rust/tw_coin_entry/src/coin_entry.rs @@ -17,6 +17,7 @@ use tw_proto::{MessageRead, MessageWrite}; pub use tw_proto::{ProtoError, ProtoResult}; +pub type PrivateKeyBytes = Data; pub type SignatureBytes = Data; pub type PublicKeyBytes = Data; diff --git a/rust/tw_coin_entry/src/error.rs b/rust/tw_coin_entry/src/error.rs index 19521b2c7b2..c31b495576b 100644 --- a/rust/tw_coin_entry/src/error.rs +++ b/rust/tw_coin_entry/src/error.rs @@ -33,6 +33,7 @@ pub enum AddressError { FromHexError, PublicKeyTypeMismatch, UnexpectedAddressPrefix, + InvalidInput, } pub type SigningResult = Result; diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index dc91228fb3b..a66a537a37d 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" lazy_static = "1.4.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" +tw_bitcoin = { path = "../tw_bitcoin" } tw_coin_entry = { path = "../tw_coin_entry" } tw_ethereum = { path = "../tw_ethereum" } tw_evm = { path = "../tw_evm" } diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index d807fc79037..47a19ff54ce 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -13,6 +13,7 @@ use std::str::FromStr; /// Extend this enum when adding new blockchains. #[derive(Copy, Clone, Debug)] pub enum BlockchainType { + Bitcoin, Ethereum, Ronin, Unsupported, @@ -33,6 +34,7 @@ impl FromStr for BlockchainType { fn from_str(s: &str) -> Result { match s { + "Bitcoin" => Ok(BlockchainType::Bitcoin), "Ethereum" => Ok(BlockchainType::Ethereum), "Ronin" => Ok(BlockchainType::Ronin), _ => Ok(BlockchainType::Unsupported), diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index d2ace9c0a52..9c47e279a02 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -9,6 +9,7 @@ use crate::coin_context::CoinRegistryContext; use crate::coin_type::CoinType; use crate::error::{RegistryError, RegistryResult}; use crate::registry::get_coin_item; +use tw_bitcoin::entry::BitcoinEntry; use tw_coin_entry::coin_entry_ext::CoinEntryExt; use tw_ethereum::entry::EthereumEntry; use tw_evm::evm_entry::EvmEntryExt; @@ -17,11 +18,13 @@ use tw_ronin::entry::RoninEntry; pub type CoinEntryExtStaticRef = &'static dyn CoinEntryExt; pub type EvmEntryExtStaticRef = &'static dyn EvmEntryExt; +const BITCOIN: BitcoinEntry = BitcoinEntry; const ETHEREUM: EthereumEntry = EthereumEntry; const RONIN: RoninEntry = RoninEntry; pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult { match blockchain { + BlockchainType::Bitcoin => Ok(&BITCOIN), BlockchainType::Ethereum => Ok(ÐEREUM), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Unsupported => Err(RegistryError::Unsupported), @@ -40,6 +43,7 @@ pub fn coin_dispatcher( pub fn evm_dispatcher(coin: CoinType) -> RegistryResult { let item = get_coin_item(coin)?; match item.blockchain { + BlockchainType::Bitcoin => Err(RegistryError::Unsupported), BlockchainType::Ethereum => Ok(ÐEREUM), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Unsupported => Err(RegistryError::Unsupported), diff --git a/rust/tw_utxo/Cargo.toml b/rust/tw_utxo/Cargo.toml new file mode 100644 index 00000000000..5631960638f --- /dev/null +++ b/rust/tw_utxo/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tw_utxo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tw_coin_entry = { path = "../tw_coin_entry" } +tw_keypair = { path = "../tw_keypair" } +tw_proto = { path = "../tw_proto" } +tw_memory = { path = "../tw_memory" } +tw_encoding = { path = "../tw_encoding" } +bitcoin = "0.30.1" +secp256k1 = { version = "0.27.0", features = [ "rand-std" ] } + +[dev-dependencies] +tw_encoding = { path = "../tw_encoding" } diff --git a/rust/tw_utxo/src/compiler.rs b/rust/tw_utxo/src/compiler.rs new file mode 100644 index 00000000000..776d5f24a32 --- /dev/null +++ b/rust/tw_utxo/src/compiler.rs @@ -0,0 +1,453 @@ +use crate::{Error, Result}; +use bitcoin::blockdata::locktime::absolute::{Height, LockTime, Time}; +use bitcoin::consensus::Encodable; +use bitcoin::hashes::Hash; +use bitcoin::sighash::{EcdsaSighashType, Prevouts, SighashCache, TapSighashType}; +use bitcoin::taproot::TapLeafHash; +use bitcoin::{OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; +use std::marker::PhantomData; +use tw_proto::Utxo::Proto::{self, SighashType}; + +type ProtoLockTimeVariant = Proto::mod_LockTime::OneOfvariant; +type ProtoSigningMethod = Proto::SigningMethod; + +pub trait UtxoContext { + type SigningInput<'a>; + type SigningOutput; + type PreSigningOutput; +} + +pub struct StandardBitcoinContext; + +impl UtxoContext for StandardBitcoinContext { + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningInput<'static>; + type PreSigningOutput = Proto::SigningInput<'static>; +} + +pub struct Compiler { + _phantom: PhantomData, +} + +impl Compiler { + #[inline] + pub fn preimage_hashes(proto: Proto::SigningInput<'_>) -> Proto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(proto) + .or_else(|err| { + std::result::Result::<_, ()>::Ok(Proto::PreSigningOutput { + error: err.into(), + ..Default::default() + }) + }) + .expect("did not convert error value") + } + + #[inline] + pub fn compile(proto: Proto::PreSerialization<'_>) -> Proto::SerializedTransaction<'static> { + Self::compile_impl(proto) + .or_else(|err| { + std::result::Result::<_, ()>::Ok(Proto::SerializedTransaction { + error: err.into(), + ..Default::default() + }) + }) + .expect("did not convert error value") + } + + fn preimage_hashes_impl( + mut proto: Proto::SigningInput<'_>, + ) -> Result> { + // TODO: Check for duplicate Txid (user error). + + // Calculate total outputs amount, based on it we can determine how many inputs to select. + let total_input: u64 = proto.inputs.iter().map(|input| input.value).sum(); + let total_output: u64 = proto.outputs.iter().map(|output| output.value).sum(); + + // Do some easy checks first. + + // Insufficient input amount. + if total_output > total_input { + return Err(Error::from(Proto::Error::Error_insufficient_inputs)); + } + + // Change scriptPubkey must be set if change output is enabled. + if !proto.disable_change_output && proto.change_script_pubkey.is_empty() { + return Err(Error::from( + Proto::Error::Error_missing_change_script_pubkey, + )); + } + + // If the input selector is InputSelector::SelectAscending, we sort the + // input first. + if let Proto::InputSelector::SelectAscending = proto.input_selector { + proto.inputs.sort_by(|a, b| a.value.cmp(&b.value)); + } + + // Unless InputSelector::UseAll is provided, we only use the necessariy + // amount of inputs to cover `total_output`. Any other input gets + // dropped. + let selected = if let Proto::InputSelector::SelectInOrder + | Proto::InputSelector::SelectAscending = proto.input_selector + { + let mut total_input = total_input; + let mut remaining = total_output; + + let selected: Vec = proto + .inputs + .into_iter() + .take_while(|input| { + if remaining == 0 { + return false; + } + + total_input += input.value; + remaining = remaining.saturating_sub(input.value); + + true + }) + .map(|input| Proto::TxIn { + txid: input.txid.to_vec().into(), + script_pubkey: input.script_pubkey.to_vec().into(), + leaf_hash: input.leaf_hash.to_vec().into(), + ..input + }) + .collect(); + + selected + } else { + // TODO: Write a function for this + proto + .inputs + .into_iter() + .map(|input| Proto::TxIn { + txid: input.txid.to_vec().into(), + script_pubkey: input.script_pubkey.to_vec().into(), + leaf_hash: input.leaf_hash.to_vec().into(), + ..input + }) + .collect() + }; + + // Update protobuf structure with selected inputs. + proto.inputs = selected.clone(); + + // Update the `total_input` amount based on the selected inputs. + let total_input: u64 = proto.inputs.iter().map(|input| input.value).sum(); + + // Calculate the total input weight projection. + let input_weight: u64 = proto.inputs.iter().map(|input| input.weight_estimate).sum(); + + // Convert Protobuf structure to `bitcoin` crate native transaction, + // used for weight/fee calculation. + let tx = convert_proto_to_tx(&proto)?; + + // Estimate of the change output weight. + let output_weight = if proto.disable_change_output { + 0 + } else { + // VarInt + script_pubkey size, rough estimate. + 1 + proto.change_script_pubkey.len() as u64 + }; + + // Calculate the full weight projection (base weight + input & output weight). + let weight_estimate = tx.weight().to_wu() + input_weight + output_weight; + let fee_estimate = (weight_estimate + 3) / 4 * proto.weight_base; + + // Check if the fee projection would make the change amount negative + // (implying insufficient input amount). + let change_amount_before_fee = total_input - total_output; + if change_amount_before_fee < fee_estimate { + return Err(Error::from(Proto::Error::Error_insufficient_inputs)); + } + + if !proto.disable_change_output { + // The amount to be returned (if enabled). + let change_amount = change_amount_before_fee - fee_estimate; + + // Update the passed on protobuf structure by adding a change output + // (return to sender) + if change_amount != 0 { + proto.outputs.push(Proto::TxOut { + value: change_amount, + script_pubkey: proto.change_script_pubkey.clone(), + }); + } + } + + // Convert *updated* Protobuf structure to `bitcoin` crate native + // transaction. + let tx = convert_proto_to_tx(&proto)?; + + let mut cache = SighashCache::new(&tx); + + let mut sighashes: Vec<(Vec, ProtoSigningMethod, Proto::SighashType)> = vec![]; + + for (index, input) in proto.inputs.iter().enumerate() { + match input.signing_method { + // Use the legacy hashing mechanism (e.g. P2SH, P2PK, P2PKH). + ProtoSigningMethod::Legacy => { + let script_pubkey = Script::from_bytes(input.script_pubkey.as_ref()); + let sighash_type = if let SighashType::UseDefault = input.sighash_type { + EcdsaSighashType::All + } else { + EcdsaSighashType::from_consensus(input.sighash_type as u32) + }; + let sighash = + cache.legacy_signature_hash(index, script_pubkey, sighash_type.to_u32())?; + + sighashes.push(( + sighash.as_byte_array().to_vec(), + ProtoSigningMethod::Legacy, + input.sighash_type, + )); + }, + // Use the Segwit hashing mechanism (e.g. P2WSH, P2WPKH). + ProtoSigningMethod::Segwit => { + let script_pubkey = ScriptBuf::from_bytes(input.script_pubkey.to_vec()); + let sighash_type = if let SighashType::UseDefault = input.sighash_type { + EcdsaSighashType::All + } else { + EcdsaSighashType::from_consensus(input.sighash_type as u32) + }; + + let sighash = cache.segwit_signature_hash( + index, + script_pubkey.as_script(), + input.value, + sighash_type, + )?; + + sighashes.push(( + sighash.as_byte_array().to_vec(), + ProtoSigningMethod::Segwit, + input.sighash_type, + )); + }, + // Use the Taproot hashing mechanism (e.g. P2TR key-path/script-path) + ProtoSigningMethod::TaprootAll => { + let leaf_hash = if input.leaf_hash.is_empty() { + None + } else { + Some(( + TapLeafHash::from_slice(input.leaf_hash.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_leaf_hash))?, + // TODO: We might want to make this configurable?. + 0xFFFFFFFF, + )) + }; + + // Note that `input.sighash_type = 0` is handled by the underlying library. + let sighash_type = TapSighashType::from_consensus_u8(input.sighash_type as u8) + .map_err(|_| Error::from(Proto::Error::Error_invalid_sighash_type))?; + + let prevouts = proto + .inputs + .iter() + .map(|i| TxOut { + value: i.value, + script_pubkey: ScriptBuf::from_bytes(i.script_pubkey.to_vec()), + }) + .collect::>(); + + let sighash = cache.taproot_signature_hash( + index, + &Prevouts::All(&prevouts), + None, + leaf_hash, + sighash_type, + )?; + + sighashes.push(( + sighash.as_byte_array().to_vec(), + ProtoSigningMethod::TaprootAll, + input.sighash_type, + )); + }, + ProtoSigningMethod::TaprootOnePrevout => { + let leaf_hash = if input.leaf_hash.is_empty() { + None + } else { + Some(( + TapLeafHash::from_slice(input.leaf_hash.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_leaf_hash))?, + // TODO: We might want to make this configurable?. + 0xFFFFFFFF, + )) + }; + + // Note that `input.sighash_type = 0` is handled by the underlying library. + let sighash_type = TapSighashType::from_consensus_u8(input.sighash_type as u8) + .map_err(|_| Error::from(Proto::Error::Error_invalid_sighash_type))?; + + let prevouts = Prevouts::One( + index, + TxOut { + value: input.value, + script_pubkey: ScriptBuf::from_bytes(input.script_pubkey.to_vec()), + }, + ); + + let sighash = cache.taproot_signature_hash( + index, + &prevouts, + None, + leaf_hash, + sighash_type, + )?; + + sighashes.push(( + sighash.as_byte_array().to_vec(), + ProtoSigningMethod::TaprootOnePrevout, + input.sighash_type, + )); + }, + } + } + + let tx = cache.into_transaction(); + // The transaction identifier, which we represent in + // non-reversed/non-network order. + let txid: Vec = tx.txid().as_byte_array().iter().copied().rev().collect(); + + Ok(Proto::PreSigningOutput { + error: Proto::Error::OK, + txid: txid.into(), + sighashes: sighashes + .into_iter() + .map(|(sighash, method, sighash_type)| Proto::Sighash { + sighash: sighash.into(), + signing_method: method, + sighash_type, + }) + .collect(), + inputs: selected, + outputs: proto + .outputs + .into_iter() + .map(|output| Proto::TxOut { + value: output.value, + script_pubkey: output.script_pubkey.to_vec().into(), + }) + .collect(), + weight_estimate, + fee_estimate, + }) + } + + fn compile_impl( + proto: Proto::PreSerialization<'_>, + ) -> Result> { + let mut tx = Transaction { + version: proto.version, + lock_time: lock_time_from_proto(&proto.lock_time)?, + input: vec![], + output: vec![], + }; + + for txin in &proto.inputs { + let txid = Txid::from_slice(txin.txid.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_txid))?; + let vout = txin.vout; + let sequence = Sequence::from_consensus(txin.sequence); + let script_sig = ScriptBuf::from_bytes(txin.script_sig.to_vec()); + let witness = Witness::from_slice( + &txin + .witness_items + .iter() + .map(|s| s.as_ref()) + .collect::>(), + ); + + tx.input.push(TxIn { + previous_output: OutPoint { txid, vout }, + script_sig, + sequence, + witness, + }); + } + + for txout in &proto.outputs { + tx.output.push(TxOut { + value: txout.value, + script_pubkey: ScriptBuf::from_bytes(txout.script_pubkey.to_vec()), + }); + } + + // Encode the transaction. + let mut buffer = vec![]; + tx.consensus_encode(&mut buffer) + .map_err(|_| Error::from(Proto::Error::Error_failed_encoding))?; + + // The transaction identifier, which we represent in + // non-reversed/non-network order. + let txid: Vec = tx.txid().as_byte_array().iter().copied().rev().collect(); + + Ok(Proto::SerializedTransaction { + error: Proto::Error::OK, + encoded: buffer.into(), + txid: txid.into(), + weight: tx.weight().to_wu(), + fee: tx.weight().to_vbytes_ceil() * proto.weight_base, + }) + } +} + +fn convert_proto_to_tx<'a>(proto: &'a Proto::SigningInput<'a>) -> Result { + let mut tx = Transaction { + version: proto.version, + lock_time: lock_time_from_proto(&proto.lock_time)?, + input: vec![], + output: vec![], + }; + + for txin in &proto.inputs { + let txid = Txid::from_slice(txin.txid.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_txid))?; + + let vout = txin.vout; + + tx.input.push(TxIn { + previous_output: OutPoint { txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence(txin.sequence), + witness: Witness::new(), + }); + } + + for txout in &proto.outputs { + tx.output.push(TxOut { + value: txout.value, + script_pubkey: ScriptBuf::from_bytes(txout.script_pubkey.to_vec()), + }); + } + + Ok(tx) +} + +// Convenience function to retreive the lock time. If none is provided, the +// default lock time is used (immediately spendable). +fn lock_time_from_proto(proto: &Option) -> Result { + let lock_time = if let Some(lock_time) = proto { + match lock_time.variant { + ProtoLockTimeVariant::blocks(block) => LockTime::Blocks( + Height::from_consensus(block) + .map_err(|_| Error::from(Proto::Error::Error_invalid_lock_time))?, + ), + ProtoLockTimeVariant::seconds(secs) => LockTime::Seconds( + Time::from_consensus(secs) + .map_err(|_| Error::from(Proto::Error::Error_invalid_lock_time))?, + ), + ProtoLockTimeVariant::None => LockTime::Blocks( + Height::from_consensus(0) + .map_err(|_| Error::from(Proto::Error::Error_invalid_lock_time))?, + ), + } + } else { + LockTime::Blocks( + Height::from_consensus(0) + .map_err(|_| Error::from(Proto::Error::Error_invalid_lock_time))?, + ) + }; + + Ok(lock_time) +} diff --git a/rust/tw_utxo/src/lib.rs b/rust/tw_utxo/src/lib.rs new file mode 100644 index 00000000000..3724b9ef75c --- /dev/null +++ b/rust/tw_utxo/src/lib.rs @@ -0,0 +1,26 @@ +use tw_proto::Utxo::Proto; + +pub mod compiler; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub struct Error(Proto::Error); + +impl From for Error { + fn from(value: Proto::Error) -> Self { + Error(value) + } +} + +impl From for Error { + fn from(_value: bitcoin::sighash::Error) -> Self { + Error(Proto::Error::Error_sighash_failed) + } +} + +impl From for Proto::Error { + fn from(value: Error) -> Self { + value.0 + } +} diff --git a/rust/tw_utxo/tests/common.rs b/rust/tw_utxo/tests/common.rs new file mode 100644 index 00000000000..c8b65535cab --- /dev/null +++ b/rust/tw_utxo/tests/common.rs @@ -0,0 +1,23 @@ +#![allow(dead_code)] + +use bitcoin::key::UntweakedPublicKey; +use bitcoin::{PubkeyHash, PublicKey, WPubkeyHash}; +use secp256k1::{hashes::Hash, XOnlyPublicKey}; +use tw_encoding::hex; + +pub fn pubkey_hash_from_hex(hex: &str) -> PubkeyHash { + PubkeyHash::from_byte_array(hex::decode(hex).unwrap().try_into().unwrap()) +} + +pub fn witness_pubkey_hash(hex: &str) -> WPubkeyHash { + WPubkeyHash::from_byte_array(hex::decode(hex).unwrap().try_into().unwrap()) +} + +pub fn untweaked_pubkey(hex: &str) -> UntweakedPublicKey { + let pubkey = PublicKey::from_slice(&hex::decode(hex).unwrap()).unwrap(); + XOnlyPublicKey::from(pubkey.inner) +} + +pub fn txid_rev(hex: &str) -> Vec { + hex::decode(hex).unwrap().into_iter().rev().collect() +} diff --git a/rust/tw_utxo/tests/input_selection.rs b/rust/tw_utxo/tests/input_selection.rs new file mode 100644 index 00000000000..a8b00d87285 --- /dev/null +++ b/rust/tw_utxo/tests/input_selection.rs @@ -0,0 +1,597 @@ +mod common; +use common::{pubkey_hash_from_hex, txid_rev}; + +use bitcoin::ScriptBuf; +use tw_proto::Utxo::Proto; +use tw_utxo::compiler::{Compiler, StandardBitcoinContext}; + +const WEIGHT_BASE: u64 = 2; + +// Convenience function, creates the change output script. +fn change_output() -> ScriptBuf { + let pubkey_hash = pubkey_hash_from_hex("aabbccddeeff00112233445566778899aabbccdd"); + ScriptBuf::new_p2pkh(&pubkey_hash) +} + +#[test] +fn input_selector_all() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 1_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx2 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 2_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx3 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 3_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 500, + script_pubkey: Default::default(), + }; + + // Generate prehashes without change output. + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // Explicitly select all inputs. + input_selector: Proto::InputSelector::UseAll, + weight_base: WEIGHT_BASE, + change_script_pubkey: Default::default(), + // DISABLE change output. + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + assert_eq!(output.sighashes.len(), 3); + + // All inputs are used as mandated by the `input_selector`. Technically only + // one input is needed. + assert_eq!(output.inputs.len(), 3); + assert_eq!(output.inputs[0], tx1); + assert_eq!(output.inputs[1], tx2); + assert_eq!(output.inputs[2], tx3); + + assert_eq!(output.outputs.len(), 1); + assert_eq!(output.outputs[0], out1); + + // Generate prehashes WITH change output. + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // Explicitly select all inputs. + input_selector: Proto::InputSelector::UseAll, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + assert_eq!(output.sighashes.len(), 3); + assert_eq!(output.weight_estimate, 594); + assert_eq!(output.fee_estimate, (594 + 3) / 4 * WEIGHT_BASE); + + assert_eq!(output.inputs.len(), 3); + assert_eq!(output.inputs[0], tx1); + assert_eq!(output.inputs[1], tx2); + assert_eq!(output.inputs[2], tx3); + + // All inputs: 6_000, all outputs: 500 + let change_out = Proto::TxOut { + value: 6_000 - 500 - output.fee_estimate, + script_pubkey: change_script.as_bytes().into(), + }; + + assert_eq!(output.outputs.len(), 2); + assert_eq!(output.outputs[0], out1); + assert_eq!(output.outputs[1], change_out); +} + +#[test] +fn input_selector_all_insufficient_inputs() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 1_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx2 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 2_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx3 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 3_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 6_000, + script_pubkey: Default::default(), + }; + + // Generate prehashes without change output. + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // Explicitly select all inputs. + input_selector: Proto::InputSelector::UseAll, + weight_base: WEIGHT_BASE, + change_script_pubkey: Default::default(), + // DISABLE change output. + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + // While the input covers all outputs, it does not + // cover the projected fee. + assert_eq!(output.error, Proto::Error::Error_insufficient_inputs); + assert_eq!(output.sighashes.len(), 0); + assert_eq!(output.inputs.len(), 0); + assert_eq!(output.outputs.len(), 0); + + // Generate prehashes WITH change output (same outcome). + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // Explicitly select all inputs. + input_selector: Proto::InputSelector::UseAll, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::Error_insufficient_inputs); + assert_eq!(output.sighashes.len(), 0); + assert_eq!(output.inputs.len(), 0); + assert_eq!(output.outputs.len(), 0); +} + +#[test] +fn input_selector_one_input_required() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 4_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx2 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 4_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx3 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 4_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 500, + script_pubkey: Default::default(), + }; + let out2 = Proto::TxOut { + value: 500, + script_pubkey: Default::default(), + }; + + // Generate sighashes without change output. + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone(), out2.clone()], + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: Default::default(), + // DISABLE change output. + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + assert_eq!(output.sighashes.len(), 1); + + // One inputs covers the full output. + assert_eq!(output.inputs.len(), 1); + assert_eq!(output.inputs[0], tx1); + + assert_eq!(output.outputs.len(), 2); + assert_eq!(output.outputs[0], out1); + assert_eq!(output.outputs[1], out2); + + // Generate sighashes WITH change output. + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone(), out2.clone()], + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + assert_eq!(output.sighashes.len(), 1); + assert_eq!(output.weight_estimate, 302); + assert_eq!(output.fee_estimate, (302 + 3) / 4 * WEIGHT_BASE); + + // One inputs covers the full output. + assert_eq!(output.inputs.len(), 1); + assert_eq!(output.inputs[0], tx1); + + // All inputs: 4_000, all outputs: 2_000 + let change_out = Proto::TxOut { + value: 4_000 - 1_000 - output.fee_estimate, + script_pubkey: change_script.as_bytes().into(), + }; + + assert_eq!(output.outputs.len(), 3); + assert_eq!(output.outputs[0], out1); + assert_eq!(output.outputs[1], out2); + assert_eq!(output.outputs[2], change_out); +} + +#[test] +fn input_selector_two_inputs_required() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 1_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx2 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 3_000, + sequence: u32::MAX, + ..Default::default() + }; + let tx3 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 4_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + let out2 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + + // Generate sighashes without change output. + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone(), out2.clone()], + // We only select the necessary value of inputs to cover the output + // value. + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: Default::default(), + // DISABLE change output. + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + //assert_eq!(output.sighashes.len(), 2); + + // Only two inputs are needed to cover outputs. + assert_eq!(output.inputs.len(), 2); + assert_eq!(output.inputs[0], tx1); + assert_eq!(output.inputs[1], tx2); + + assert_eq!(output.outputs.len(), 2); + assert_eq!(output.outputs[0], out1); + assert_eq!(output.outputs[1], out2); + + // Generate sighashes WITH change output. + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone(), out2.clone()], + // We only select the necessary value of inputs to cover the output + // value. + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + assert_eq!(output.sighashes.len(), 2); + assert_eq!(output.weight_estimate, 466); + assert_eq!(output.fee_estimate, (466 + 3) / 4 * WEIGHT_BASE); + + // Only two inputs are needed to cover outputs. + assert_eq!(output.inputs.len(), 2); + assert_eq!(output.inputs[0], tx1); + assert_eq!(output.inputs[1], tx2); + + // All inputs: 4_000, all outputs: 2_000 + let change_out = Proto::TxOut { + value: 4_000 - 2_000 - output.fee_estimate, + script_pubkey: change_script.as_bytes().into(), + }; + + assert_eq!(output.outputs.len(), 3); + assert_eq!(output.outputs[0], out1); + assert_eq!(output.outputs[1], out2); + assert_eq!(output.outputs[2], change_out); +} + +#[test] +fn input_selector_one_input_cannot_cover_fees() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 2_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + let out2 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + + // Generate sighashes without change output. + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone()], + outputs: vec![out1.clone(), out2.clone()], + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: Default::default(), + // DISABLE change output. + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + // While the input covers all outputs, it does not + // cover the projected fee. + assert_eq!(output.error, Proto::Error::Error_insufficient_inputs); + assert_eq!(output.weight_estimate, 0); + assert_eq!(output.fee_estimate, 0); + assert_eq!(output.sighashes.len(), 0); + assert_eq!(output.inputs.len(), 0); + assert_eq!(output.outputs.len(), 0); + + // Generate sighashes WITH change output (same outcome). + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone()], + outputs: vec![out1.clone(), out2.clone()], + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::Error_insufficient_inputs); + assert_eq!(output.sighashes.len(), 0); + assert_eq!(output.weight_estimate, 0); + assert_eq!(output.fee_estimate, 0); + assert_eq!(output.inputs.len(), 0); + assert_eq!(output.outputs.len(), 0); +} + +#[test] +fn input_selector_exact_balance_no_change() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + // Covers the exact output value + projected fee. + value: 2_000 + (302 + 3) / 4 * WEIGHT_BASE, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + let out2 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone()], + outputs: vec![out1.clone(), out2.clone()], + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + assert_eq!(output.sighashes.len(), 1); + assert_eq!(output.weight_estimate, 302); + assert_eq!(output.fee_estimate, (302 + 3) / 4 * WEIGHT_BASE); + + // One inputs covers the full output. + assert_eq!(output.inputs.len(), 1); + assert_eq!(output.inputs[0], tx1); + + // NO change output + assert_eq!(output.outputs.len(), 2); + assert_eq!(output.outputs[0], out1); + assert_eq!(output.outputs[1], out2); +} + +#[test] +fn input_selector_empty_script_bufs() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 4_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + let out2 = Proto::TxOut { + value: 1_000, + script_pubkey: Default::default(), + }; + + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone()], + outputs: vec![out1.clone(), out2.clone()], + input_selector: Proto::InputSelector::SelectInOrder, + weight_base: WEIGHT_BASE, + // NO change script_pubkey specified, results in an error. + change_script_pubkey: Default::default(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!( + output.error, + Proto::Error::Error_missing_change_script_pubkey + ); + assert_eq!(output.sighashes.len(), 0); + assert_eq!(output.weight_estimate, 0); + assert_eq!(output.fee_estimate, 0); + assert_eq!(output.inputs.len(), 0); + assert_eq!(output.outputs.len(), 0); +} + +#[test] +fn input_selector_select_ascending() { + // Reusing the txid is fine here, although in production this would mark the transaction invalid. + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let tx1 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 8_000, + sequence: u32::MAX, + ..Default::default() + }; + + let tx2 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 4_000, + sequence: u32::MAX, + ..Default::default() + }; + + let tx3 = Proto::TxIn { + txid: txid.as_slice().into(), + value: 2_000, + sequence: u32::MAX, + ..Default::default() + }; + + let out1 = Proto::TxOut { + value: 3_000, + script_pubkey: Default::default(), + }; + + let change_script = change_output(); + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // Select in ASCENDING order: + input_selector: Proto::InputSelector::SelectAscending, + weight_base: WEIGHT_BASE, + change_script_pubkey: change_script.as_bytes().into(), + // ENABLE change output. + disable_change_output: false, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + + // Two inputs where selected (in ASCENDING order). + assert_eq!(output.inputs.len(), 2); + assert_eq!(output.inputs[0], tx3); + assert_eq!(output.inputs[1], tx2); + + // Two outputs; target and change. + assert_eq!(output.outputs.len(), 2); + assert_eq!(output.outputs[0], out1); +} diff --git a/rust/tw_utxo/tests/p2pkh.rs b/rust/tw_utxo/tests/p2pkh.rs new file mode 100644 index 00000000000..99fe20e1cc0 --- /dev/null +++ b/rust/tw_utxo/tests/p2pkh.rs @@ -0,0 +1,53 @@ +mod common; +use common::{pubkey_hash_from_hex, txid_rev}; + +use bitcoin::ScriptBuf; +use tw_encoding::hex; +use tw_proto::Utxo::Proto; +use tw_utxo::compiler::{Compiler, StandardBitcoinContext}; + +#[test] +fn sighash_input_p2pkh_output_p2pkh() { + let pubkey_hash = pubkey_hash_from_hex("e4c1ea86373d554b8f4efff2cfb0001ea19124d2"); + let input_script_pubkey = ScriptBuf::new_p2pkh(&pubkey_hash); + + let pubkey_hash = pubkey_hash_from_hex("5eaaa4f458f9158f86afcba08dd7448d27045e3d"); + let output_script_pubkey = ScriptBuf::new_p2pkh(&pubkey_hash); + + let txid = txid_rev("1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"); + + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![Proto::TxIn { + txid: txid.into(), + vout: 0, + // Amount is not part of sighash for `Legacy`. + value: u64::MAX, + sequence: u32::MAX, + script_pubkey: input_script_pubkey.as_bytes().into(), + sighash_type: Proto::SighashType::All, + signing_method: Proto::SigningMethod::Legacy, + weight_estimate: 1, + leaf_hash: Default::default(), + }], + outputs: vec![Proto::TxOut { + value: 50 * 100_000_000 - 1_000_000, + script_pubkey: output_script_pubkey.as_bytes().into(), + }], + input_selector: Proto::InputSelector::UseAll, + weight_base: 1, + change_script_pubkey: Default::default(), + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + + let hashes = output.sighashes; + assert_eq!(hashes.len(), 1); + assert_eq!( + hex::encode(hashes[0].sighash.as_ref(), false), + "6a0e072da66b141fdb448323d54765cafcaf084a06d2fa13c8aed0c694e50d18" + ); +} diff --git a/rust/tw_utxo/tests/p2tr.rs b/rust/tw_utxo/tests/p2tr.rs new file mode 100644 index 00000000000..f8521872f33 --- /dev/null +++ b/rust/tw_utxo/tests/p2tr.rs @@ -0,0 +1,56 @@ +mod common; +use common::{pubkey_hash_from_hex, txid_rev, untweaked_pubkey}; + +use bitcoin::ScriptBuf; +use tw_encoding::hex; +use tw_proto::Utxo::Proto; +use tw_utxo::compiler::{Compiler, StandardBitcoinContext}; + +#[test] +fn sighash_input_p2pkh_output_p2tr_key_spend() { + let pubkey_hash = pubkey_hash_from_hex("a0cd6d6e2f9804351ba4b722b708bc2fd3229a5a"); + let input_script_pubkey = ScriptBuf::new_p2pkh(&pubkey_hash); + + let untweaked_pubkey = + untweaked_pubkey("02c0938cf377023dfde55e9c96b3cff4ca8894fb6b5d2009006bd43c0bff69cac9"); + let output_script_pubkey = + // Merkle root of `None` is interpreted as P2TR key-spend. + ScriptBuf::new_v1_p2tr(&secp256k1::Secp256k1::new(), untweaked_pubkey, None); + + let txid = txid_rev("c50563913e5a838f937c94232f5a8fc74e58b629fae41dfdffcc9a70f833b53a"); + + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![Proto::TxIn { + txid: txid.into(), + vout: 0, + // Amount is not part of sighash for `Legacy`. + value: u64::MAX, + sequence: u32::MAX, + script_pubkey: input_script_pubkey.as_bytes().into(), + sighash_type: Proto::SighashType::All, + signing_method: Proto::SigningMethod::Legacy, + weight_estimate: 1, + leaf_hash: Default::default(), + }], + outputs: vec![Proto::TxOut { + value: 50 * 100_000_000 - 1_000_000, + script_pubkey: output_script_pubkey.as_bytes().into(), + }], + input_selector: Proto::InputSelector::UseAll, + weight_base: 1, + change_script_pubkey: Default::default(), + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + + let hashes = output.sighashes; + assert_eq!(hashes.len(), 1); + assert_eq!( + hex::encode(hashes[0].sighash.as_ref(), false), + "c914fd08efdcc7f8007c75c39ab47e1ee736a6ce1e6363250fe88cda8fca04d1" + ); +} diff --git a/rust/tw_utxo/tests/p2wpkh.rs b/rust/tw_utxo/tests/p2wpkh.rs new file mode 100644 index 00000000000..3ad47370695 --- /dev/null +++ b/rust/tw_utxo/tests/p2wpkh.rs @@ -0,0 +1,100 @@ +mod common; +use common::{pubkey_hash_from_hex, txid_rev, witness_pubkey_hash}; + +use bitcoin::ScriptBuf; +use tw_encoding::hex; +use tw_proto::Utxo::Proto; +use tw_utxo::compiler::{Compiler, StandardBitcoinContext}; + +#[test] +fn sighash_input_p2pkh_output_p2wpkh() { + let pubkey_hash = pubkey_hash_from_hex("60cda7b50f14c152d7401c28ae773c698db92373"); + let input_script_pubkey = ScriptBuf::new_p2pkh(&pubkey_hash); + + let wpubkey_hash = witness_pubkey_hash("0d0e1cec6c2babe8badde5e9b3dea667da90036d"); + let output_script_pubkey = ScriptBuf::new_v0_p2wpkh(&wpubkey_hash); + + let txid = txid_rev("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911"); + + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![Proto::TxIn { + txid: txid.into(), + vout: 0, + // Amount is not part of sighash for `Legacy`. + value: u64::MAX, + sequence: u32::MAX, + script_pubkey: input_script_pubkey.as_bytes().into(), + sighash_type: Proto::SighashType::All, + signing_method: Proto::SigningMethod::Legacy, + weight_estimate: 1, + leaf_hash: Default::default(), + }], + outputs: vec![Proto::TxOut { + value: 50 * 100_000_000 - 1_000_000, + script_pubkey: output_script_pubkey.as_bytes().into(), + }], + input_selector: Proto::InputSelector::UseAll, + weight_base: 1, + change_script_pubkey: Default::default(), + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + + let hashes = output.sighashes; + assert_eq!(hashes.len(), 1); + assert_eq!( + hex::encode(hashes[0].sighash.as_ref(), false), + "c4963ecd6c08be4c9dd66416349084a5b54318b3802370451d580210bc883463" + ); +} + +#[test] +fn sighash_input_p2wpkh_output_p2wpkh() { + let wpubkey_hash = witness_pubkey_hash("0d0e1cec6c2babe8badde5e9b3dea667da90036d"); + let input_script_pubkey = ScriptBuf::new_v0_p2wpkh(&wpubkey_hash) + .p2wpkh_script_code() + .unwrap(); + + let wpubkey_hash = witness_pubkey_hash("60cda7b50f14c152d7401c28ae773c698db92373"); + let output_script_pubkey = ScriptBuf::new_v0_p2wpkh(&wpubkey_hash); + + let txid = txid_rev("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e"); + + let signing = Proto::SigningInput { + version: 2, + lock_time: Default::default(), + inputs: vec![Proto::TxIn { + txid: txid.into(), + vout: 0, + value: 50 * 100_000_000 - 1_000_000, + sequence: u32::MAX, + script_pubkey: input_script_pubkey.as_bytes().into(), + sighash_type: Proto::SighashType::All, + signing_method: Proto::SigningMethod::Segwit, + weight_estimate: 1, + leaf_hash: Default::default(), + }], + outputs: vec![Proto::TxOut { + value: 50 * 100_000_000 - 1_000_000 * 2, + script_pubkey: output_script_pubkey.as_bytes().into(), + }], + input_selector: Proto::InputSelector::UseAll, + weight_base: 1, + change_script_pubkey: Default::default(), + disable_change_output: true, + }; + + let output = Compiler::::preimage_hashes(signing); + assert_eq!(output.error, Proto::Error::OK); + + let hashes = output.sighashes; + assert_eq!(hashes.len(), 1); + assert_eq!( + hex::encode(hashes[0].sighash.as_ref(), false), + "6900ebbef74c938ec2310df10cd520b5e7c82c0fe1bb68c62c8fae7bf54e2092" + ); +} diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index 11f02172bd5..da96b7e588d 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -8,12 +8,14 @@ name = "wallet_core_rs" crate-type = ["staticlib", "rlib"] # Creates static lib [features] -default = ["ethereum-rlp"] +default = ["bitcoin-legacy", "ethereum-rlp"] +bitcoin-legacy = [] ethereum-rlp = [] [dependencies] tw_any_coin = { path = "../tw_any_coin" } tw_bitcoin = { path = "../tw_bitcoin" } +tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } tw_coin_registry = { path = "../tw_coin_registry" } tw_encoding = { path = "../tw_encoding" } tw_ethereum = { path = "../tw_ethereum" } @@ -26,6 +28,5 @@ tw_proto = { path = "../tw_proto" } [dev-dependencies] tw_any_coin = { path = "../tw_any_coin", features = ["test-utils"] } -tw_coin_entry = { path = "../tw_coin_entry" } tw_memory = { path = "../tw_memory", features = ["test-utils"] } tw_number = { path = "../tw_number", features = ["helpers"] } diff --git a/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs b/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs new file mode 100644 index 00000000000..5a172e2c648 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs @@ -0,0 +1,308 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#![allow(clippy::missing_safety_doc)] + +use std::ffi::{c_char, CStr}; +use tw_bitcoin::aliases::*; +use tw_bitcoin::native::consensus::Decodable; +use tw_bitcoin::native::{PublicKey, Transaction}; +use tw_memory::ffi::c_byte_array::CByteArray; +use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; +use tw_memory::ffi::c_result::CUInt64Result; +use tw_misc::try_or_else; +use tw_proto::Bitcoin::Proto as LegacyProto; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Common::Proto as CommonProto; + +// NOTE: The tests for those APIs can be found in `tw_bitcoin`. + +#[no_mangle] +#[deprecated] +// Builds the P2PKH scriptPubkey. +pub unsafe extern "C" fn tw_bitcoin_legacy_build_p2pkh_script( + _satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + let recipient = try_or_else!(PublicKey::from_slice(slice), CByteArray::null); + + let output = Proto::Output { + value: _satoshis as u64, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(recipient.to_bytes().into()), + }), + }), + }; + + let res = try_or_else!( + tw_bitcoin::modules::transactions::OutputBuilder::utxo_from_proto(&output), + CByteArray::null + ); + + // Prepare and serialize protobuf structure. + let proto = LegacyProto::TransactionOutput { + value: res.value as i64, + script: res.script_pubkey, + spendingScript: Default::default(), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + CByteArray::from(serialized) +} + +#[no_mangle] +#[deprecated] +// Builds the P2WPKH scriptPubkey. +pub unsafe extern "C" fn tw_bitcoin_legacy_build_p2wpkh_script( + _satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + + let recipient = try_or_else!(PublicKey::from_slice(slice), CByteArray::null); + + let output = Proto::Output { + value: _satoshis as u64, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(recipient.to_bytes().into()), + }), + }), + }; + + let res = try_or_else!( + tw_bitcoin::modules::transactions::OutputBuilder::utxo_from_proto(&output), + CByteArray::null + ); + + // Prepare and serialize protobuf structure. + let proto = LegacyProto::TransactionOutput { + value: res.value as i64, + script: res.script_pubkey, + spendingScript: Default::default(), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + CByteArray::from(serialized) +} + +#[no_mangle] +#[deprecated] +// Builds the P2TR key-path scriptPubkey. +pub unsafe extern "C" fn tw_bitcoin_legacy_build_p2tr_key_path_script( + _satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + let recipient = try_or_else!(PublicKey::from_slice(slice), CByteArray::null); + + let output = Proto::Output { + value: _satoshis as u64, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2tr_key_path(recipient.to_bytes().into()), + }), + }; + + let res = try_or_else!( + tw_bitcoin::modules::transactions::OutputBuilder::utxo_from_proto(&output), + CByteArray::null + ); + + // Prepare and serialize protobuf structure. + let proto = LegacyProto::TransactionOutput { + value: res.value as i64, + script: res.script_pubkey, + spendingScript: Default::default(), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + CByteArray::from(serialized) +} + +#[no_mangle] +#[deprecated] +// Builds the Ordinals inscripton for BRC20 transfer. +pub unsafe extern "C" fn tw_bitcoin_legacy_build_brc20_transfer_inscription( + // The 4-byte ticker. + ticker: *const c_char, + value: u64, + _satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + + let recipient = try_or_else!(PublicKey::from_slice(slice), CByteArray::null); + + // Convert ticket. + let ticker = match CStr::from_ptr(ticker).to_str() { + Ok(input) => input, + Err(_) => return CByteArray::null(), + }; + + let output = Proto::Output { + value: _satoshis as u64, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::brc20_inscribe( + Proto::mod_Output::OutputBrc20Inscription { + inscribe_to: recipient.to_bytes().into(), + ticker: ticker.into(), + transfer_amount: value, + }, + ), + }), + }; + + let res = try_or_else!( + tw_bitcoin::modules::transactions::OutputBuilder::utxo_from_proto(&output), + CByteArray::null + ); + + // Prepare and serialize protobuf structure. + let proto = LegacyProto::TransactionOutput { + value: res.value as i64, + script: res.script_pubkey, + spendingScript: res.taproot_payload, + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + CByteArray::from(serialized) +} + +#[no_mangle] +#[deprecated] +// Builds the Ordinals inscripton for BRC20 transfer. +pub unsafe extern "C" fn tw_bitcoin_legacy_build_nft_inscription( + mime_type: *const c_char, + payload: *const u8, + payload_len: usize, + _satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert mimeType. + let mime_type = match CStr::from_ptr(mime_type).to_str() { + Ok(input) => input, + Err(_) => return CByteArray::null(), + }; + + // Convert data to inscribe. + let payload = try_or_else!( + CByteArrayRef::new(payload, payload_len).as_slice(), + CByteArray::null + ); + + // Convert Recipient. + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + + let recipient = try_or_else!(PublicKey::from_slice(slice), CByteArray::null); + + // Inscribe NFT data. + let output = Proto::Output { + value: _satoshis as u64, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::ordinal_inscribe( + Proto::mod_Output::OutputOrdinalInscription { + inscribe_to: recipient.to_bytes().into(), + mime_type: mime_type.into(), + payload: payload.into(), + }, + ), + }), + }; + + let res = try_or_else!( + tw_bitcoin::modules::transactions::OutputBuilder::utxo_from_proto(&output), + CByteArray::null + ); + + // Prepare and serialize protobuf structure. + let proto = LegacyProto::TransactionOutput { + value: res.value as i64, + script: res.script_pubkey, + spendingScript: res.taproot_payload, + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + CByteArray::from(serialized) +} + +#[deprecated] +#[no_mangle] +pub unsafe extern "C" fn tw_bitcoin_legacy_calculate_transaction_fee( + input: *const u8, + input_len: usize, + sat_vb: u64, +) -> CUInt64Result { + let Some(mut encoded) = CByteArrayRef::new(input, input_len).as_slice() else { + return CUInt64Result::error(1); + }; + + // Decode transaction. + let Ok(tx) = Transaction::consensus_decode(&mut encoded) else { + return CUInt64Result::error(1); + }; + + // Calculate fee. + let weight = tx.weight(); + let fee = weight.to_vbytes_ceil() * sat_vb; + + CUInt64Result::ok(fee) +} + +#[no_mangle] +#[deprecated] +pub unsafe extern "C" fn tw_bitcoin_legacy_taproot_build_and_sign_transaction( + input: *const u8, + input_len: usize, +) -> CByteArray { + let data = CByteArrayRef::new(input, input_len) + .to_vec() + .unwrap_or_default(); + + let proto: LegacyProto::SigningInput = + try_or_else!(tw_proto::deserialize(&data), CByteArray::null); + + let Ok(signing) = tw_bitcoin::modules::legacy::taproot_build_and_sign_transaction(proto) else { + // Convert the `BitcoinV2.proto` error type inot the `Common.proto` + // errot type and return. + let error = LegacyProto::SigningOutput { + error: CommonProto::SigningError::Error_general, + ..Default::default() + }; + + let serialized = tw_proto::serialize(&error).expect("failed to serialize error message"); + return CByteArray::from(serialized) + }; + + // Serialize SigningOutput and return. + let serialized = tw_proto::serialize(&signing).expect("failed to serialize signed transaction"); + CByteArray::from(serialized) +} diff --git a/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs b/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs new file mode 100644 index 00000000000..5b0dcab0885 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs @@ -0,0 +1,8 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#[cfg(feature = "bitcoin-legacy")] +pub mod legacy; diff --git a/rust/wallet_core_rs/src/ffi/mod.rs b/rust/wallet_core_rs/src/ffi/mod.rs index 1ee22be0aeb..54176826c15 100644 --- a/rust/wallet_core_rs/src/ffi/mod.rs +++ b/rust/wallet_core_rs/src/ffi/mod.rs @@ -4,4 +4,5 @@ // terms governing use, modification, and redistribution, is contained in the // file LICENSE at the root of the source code distribution tree. +pub mod bitcoin; pub mod ethereum; diff --git a/samples/kmp/shared/build.gradle.kts b/samples/kmp/shared/build.gradle.kts index fa64d9e8028..8fb2eec545f 100644 --- a/samples/kmp/shared/build.gradle.kts +++ b/samples/kmp/shared/build.gradle.kts @@ -35,7 +35,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("com.trustwallet:wallet-core-kotlin:3.2.12") + implementation("com.trustwallet:wallet-core-kotlin:3.2.20") } } val commonTest by getting { diff --git a/src/Bitcoin/Script.cpp b/src/Bitcoin/Script.cpp index 0122f6d5eb6..86bde0a6669 100644 --- a/src/Bitcoin/Script.cpp +++ b/src/Bitcoin/Script.cpp @@ -530,7 +530,7 @@ Script Script::lockScriptForAddress(const std::string& string, enum TWCoinType c Proto::TransactionOutput Script::buildBRC20InscribeTransfer(const std::string& ticker, uint64_t amount, const Data& publicKey) { TW::Bitcoin::Proto::TransactionOutput out; - Rust::CByteArrayWrapper res = TW::Rust::tw_build_brc20_transfer_inscription(ticker.data(), amount, 0, publicKey.data(), publicKey.size()); + Rust::CByteArrayWrapper res = TW::Rust::tw_bitcoin_legacy_build_brc20_transfer_inscription(ticker.data(), amount, 0, publicKey.data(), publicKey.size()); auto result = res.data; out.ParseFromArray(result.data(), static_cast(result.size())); return out; @@ -538,7 +538,7 @@ Proto::TransactionOutput Script::buildBRC20InscribeTransfer(const std::string& t Proto::TransactionOutput Script::buildOrdinalNftInscription(const std::string& mimeType, const Data& payload, const Data& publicKey) { TW::Bitcoin::Proto::TransactionOutput out; - Rust::CByteArrayWrapper res = TW::Rust::tw_bitcoin_build_nft_inscription( + Rust::CByteArrayWrapper res = TW::Rust::tw_bitcoin_legacy_build_nft_inscription( mimeType.data(), payload.data(), payload.size(), diff --git a/src/Bitcoin/Signer.cpp b/src/Bitcoin/Signer.cpp index ff33e295c98..0781237f862 100644 --- a/src/Bitcoin/Signer.cpp +++ b/src/Bitcoin/Signer.cpp @@ -25,7 +25,7 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput& input, std::optiona Proto::SigningOutput output; if (input.is_it_brc_operation()) { auto serializedInput = data(input.SerializeAsString()); - Rust::CByteArrayWrapper res = Rust::tw_taproot_build_and_sign_transaction(serializedInput.data(), serializedInput.size()); + Rust::CByteArrayWrapper res = Rust::tw_bitcoin_legacy_taproot_build_and_sign_transaction(serializedInput.data(), serializedInput.size()); output.ParseFromArray(res.data.data(), static_cast(res.data.size())); return output; } diff --git a/src/Bitcoin/Transaction.cpp b/src/Bitcoin/Transaction.cpp index 46dd2f4ba93..3a0cc8a4ac6 100644 --- a/src/Bitcoin/Transaction.cpp +++ b/src/Bitcoin/Transaction.cpp @@ -239,7 +239,7 @@ void Transaction::serializeInput(size_t subindex, const Script& scriptCode, size } std::optional Transaction::calculateFee(const Data& encoded, uint64_t satVb) { - Rust::CUInt64ResultWrapper res = Rust::tw_bitcoin_calculate_transaction_fee(encoded.data(), encoded.size(), satVb); + Rust::CUInt64ResultWrapper res = Rust::tw_bitcoin_legacy_calculate_transaction_fee(encoded.data(), encoded.size(), satVb); if (res.isErr()) { return std::nullopt; } diff --git a/src/proto/BitcoinV2.proto b/src/proto/BitcoinV2.proto new file mode 100644 index 00000000000..c55810557bf --- /dev/null +++ b/src/proto/BitcoinV2.proto @@ -0,0 +1,430 @@ +syntax = "proto3"; + +package TW.BitcoinV2.Proto; +option java_package = "wallet.core.jni.proto"; + +import "Utxo.proto"; + +enum Error { + OK = 0; + // `tx_utxo` related errors. + Error_utxo_invalid_leaf_hash = 2; + Error_utxo_invalid_sighash_type = 3; + Error_utxo_invalid_lock_time = 4; + Error_utxo_invalid_txid = 5; + Error_utxo_sighash_failed = 6; + Error_utxo_missing_sighash_method = 7; + Error_utxo_failed_encoding = 8; + Error_utxo_insufficient_inputs = 9; + Error_utxo_missing_change_script_pubkey = 10; + // `tw_bitcoin` related errors. + Error_zero_sequence_not_enabled = 11; + Error_unmatched_input_signature_count = 12; + Error_missing_input_builder = 13; + Error_missing_output_builder = 14; + Error_missing_recipient = 15; + Error_missing_inscription = 41; + Error_missing_tagged_output = 42; + Error_legacy_p2tr_invalid_variant = 16; + Error_legacy_no_spending_script_provided = 17; + Error_legacy_expected_redeem_script = 18; + Error_legacy_outpoint_not_set = 19; + Error_legacy_no_private_key = 36; + Error_legacy_no_plan_provided = 37; + Error_invalid_private_key = 20; + Error_invalid_public_key = 21; + Error_invalid_sighash = 22; + Error_invalid_witness_pubkey_hash = 23; + Error_invalid_brc20_ticker = 24; + Error_invalid_ecdsa_signature = 25; + Error_invalid_schnorr_signature = 26; + Error_invalid_control_block = 27; + Error_invalid_pubkey_hash = 28; + Error_invalid_taproot_root = 29; + Error_invalid_redeem_script = 30; + Error_invalid_wpkh_script_code = 1; + Error_invalid_witness_redeem_script_hash = 31; + Error_invalid_witness_encoding = 39; + Error_invalid_taproot_tweaked_pubkey = 32; + Error_invalid_change_output = 33; + Error_unsupported_address_recipient = 34; + Error_bad_address_recipient = 35; + Error_ordinal_mime_type_too_large = 38; + Error_ordinal_payload_too_large = 40; +} + +message SigningInput { + // (optional) The protocol version, is currently expected to be 1 or 2. + // Version 2 by default. + int32 version = 1; + + // Only required if the `sign` method is called. + bytes private_key = 2; + + // (optional) Block height or timestamp indicating at what point transactions can be + // included in a block. None by default (zero value). + Utxo.Proto.LockTime lock_time = 3; + + // The inputs to spend. + repeated Input inputs = 5; + + // The output of the transaction. Note that the change output is specified + // in the `change_output` field. + repeated Output outputs = 6; + + // How the inputs should be selected. + Utxo.Proto.InputSelector input_selector = 7; + + // (optional) The amount of satoshis per vbyte ("satVb"), used for fee calculation. + uint64 fee_per_vb = 8; + + // The change output to be added (return to sender). + // The `value` can be left at 0. + Output change_output = 9; + + // Explicility disable change output creation. + bool disable_change_output = 10; + + bool dangerous_use_fixed_schnorr_rng = 11; +} + +message Input { + // Use an individual private key for this input. Only required if the `sign` + // method is called. + bytes private_key = 1; + + // The referenced transaction ID in REVERSED order. + bytes txid = 2; + + // The position in the previous transactions output that this input + // references. + uint32 vout = 3; + + // The sequence number, used for timelocks, replace-by-fee, etc. Normally + // this number is simply 4294967295 (0xFFFFFFFF) . + uint32 sequence = 4; + + // If the sequence is a zero value, this field must be set to `true`. + bool sequence_enable_zero = 5; + + // The amount of satoshis of this input. Required for producing + // Segwit/Taproot transactions. + uint64 value = 6; + + // The sighash type, normally `SighashType::UseDefault` (All). + Utxo.Proto.SighashType sighash_type = 7; + + // The reciepient of this input (the spender) + oneof to_recipient { + // Construct input with a buildler pattern. + InputBuilder builder = 8; + // Construct input by providing raw spending information directly. + InputScriptWitness custom_script = 9; + } + + message InputBuilder { + oneof variant { + // Pay-to-Script-Hash, specify the redeem script. + bytes p2sh = 1; + // Pay-to-Public-Key-Hash, specify the public key. + bytes p2pkh = 2; + // Pay-to-Witness-Script-Hash, specify the redeem script. + bytes p2wsh = 3; + // Pay-to-Public-Key-Hash, specify the public key. + bytes p2wpkh = 6; + // Pay-to-Taproot-key-path (balance transfers). + InputTaprootKeyPath p2tr_key_path = 7; + // Pay-to-Taproot-script-path (complex transfers). + InputTaprootScriptPath p2tr_script_path = 8; + // Create a BRC20 inscription. + InputBrc20Inscription brc20_inscribe = 9; + // Create an Ordinal (NFT) inscriptiohn. + InputOrdinalInscription ordinal_inscribe = 10; + } + } + + message InputScriptWitness { + // The spending condition of this input. + bytes script_pubkey = 1; + // The claiming script for this input (non-Segwit/non-Taproot) + bytes script_sig = 2; + // The claiming script for this input (Segwit/Taproot) + repeated bytes witness_items = 3; + // The signing method. + Utxo.Proto.SigningMethod signing_method = 5; + } + + message InputTaprootKeyPath { + // Whether only one prevout should be used to calculate the Sighash. + // Normally this is `false`. + bool one_prevout = 1; + // The recipient. + bytes public_key = 2; + } + + message InputTaprootScriptPath { + // Whether only one prevout should be used to calculate the Sighash. + // Normally this is `false`. + bool one_prevout = 1; + // The payload of the Taproot transaction. + bytes payload = 2; + // The control block of the Taproot transaction required for claiming. + bytes control_block = 3; + } + + message InputOrdinalInscription { + // Whether only one prevout should be used to calculate the Sighash. + // Normally this is `false`. + bool one_prevout = 1; + // The recipient of the inscription, usually the sender. + bytes inscribe_to = 2; + // The MIME type of the inscription, such as `image/png`, etc. + string mime_type = 3; + // The actual inscription content. + bytes payload = 4; + } + + message InputBrc20Inscription { + bool one_prevout = 1; + // The recipient of the inscription, usually the sender. + bytes inscribe_to = 2; + // The ticker of the BRC20 inscription. + string ticker = 3; + // The BRC20 token transfer amount. + uint64 transfer_amount = 4; + } +} + +message Output { + // The amount of satoshis to spend. + uint64 value = 1; + + oneof to_recipient { + // Construct output with builder pattern. + OutputBuilder builder = 2; + // Construct output by providing the scriptPubkey directly. + bytes custom_script_pubkey = 3; + // Derive the expected output from the provided address. + string from_address = 4; + } + + message OutputBuilder { + oneof variant { + // Pay-to-Script-Hash, specify the hash. + OutputRedeemScriptOrHash p2sh = 1; + // Pay-to-Public-Key-Hash + ToPublicKeyOrHash p2pkh = 2; + // Pay-to-Witness-Script-Hash, specify the hash. + OutputRedeemScriptOrHash p2wsh = 3; + // Pay-to-Public-Key-Hash + ToPublicKeyOrHash p2wpkh = 4; + // Pay-to-Taproot-key-path (balance transfers), specify the public key. + bytes p2tr_key_path = 5; + // Pay-to-Taproot-script-path (complex transfers) + OutputTaprootScriptPath p2tr_script_path = 6; + bytes p2tr_dangerous_assume_tweaked = 7; + OutputBrc20Inscription brc20_inscribe = 8; + OutputOrdinalInscription ordinal_inscribe = 9; + } + } + + message OutputRedeemScriptOrHash { + oneof variant { + bytes redeem_script = 1; + bytes hash = 2; + } + } + + message OutputTaprootScriptPath { + // The internal key, usually the public key of the recipient. + bytes internal_key = 1; + // The merkle root of the Taproot script(s), required to compute the sighash. + bytes merkle_root = 2; + } + + message OutputOrdinalInscription { + // The recipient of the inscription, usually the sender. + bytes inscribe_to = 1; + // The MIME type of the inscription, such as `image/png`, etc. + string mime_type = 2; + // The actual inscription content. + bytes payload = 3; + } + + message OutputBrc20Inscription { + // The recipient of the inscription, usually the sender. + bytes inscribe_to = 1; + // The ticker of the BRC20 inscription. + string ticker = 2; + // The BRC20 token transfer amount. + uint64 transfer_amount = 3; + } +} + +message ToPublicKeyOrHash { + oneof to_address { + bytes pubkey = 1; + bytes hash = 2; + } +} + +message PreSigningOutput { + // A possible error, `OK` if none. + Error error = 1; + + string error_message = 2; + + // The transaction ID in NON-reversed order. Note that this must be reversed + // when referencing in future transactions. + bytes txid = 3; + + /// The sighashes to be signed; ECDSA for legacy and Segwit, Schnorr for Taproot. + repeated Utxo.Proto.Sighash sighashes = 4; + + // The raw inputs. + repeated Utxo.Proto.TxIn utxo_inputs = 5; + + // The raw outputs. + repeated TxOut utxo_outputs = 6; + + // The estimated weight of the transaction. + uint64 weight_estimate = 7; + + // The estimated fees of the transaction in satoshis. + uint64 fee_estimate = 8; + + // The output of a transaction. + message TxOut { + // The value of the output (in satoshis). + uint64 value = 1; + // The spending condition of the output. + bytes script_pubkey = 2; + // The payload of the Taproot script. + bytes taproot_payload = 3; + // The optional control block for a Taproot output (P2TR script-path). + bytes control_block = 4; + } +} + +message SigningOutput { + // A possible error, `OK` if none. + Error error = 1; + + string error_message = 2; + + Transaction transaction = 3; + + // The encoded transaction that submitted to the network. + bytes encoded = 4; + + // The transaction ID in NON-reversed order. Note that this must be reversed + // when referencing in future transactions. + bytes txid = 5; + + // The total and final weight of the transaction. + uint64 weight = 6; + + // The total and final fee of the transaction in satoshis. + uint64 fee = 7; +} + +message Transaction { + // The protocol version, is currently expected to be 1 or 2 (BIP68) + int32 version = 1; + + // Block height or timestamp indicating at what point transactions can be + // included in a block. None by default (zero value). + Utxo.Proto.LockTime lock_time = 2; + + // The transaction inputs. + repeated TransactionInput inputs = 3; + + // The transaction outputs. + repeated TransactionOutput outputs = 4; +} + +message TransactionInput { + // The referenced transaction ID in REVERSED order. + bytes txid = 1; + + // The position in the previous transactions output that this input + // references. + uint32 vout = 3; + + // The sequence number, used for timelocks, replace-by-fee, etc. Normally + // this number is simply 4294967295 (0xFFFFFFFF) . + uint32 sequence = 4; + + // The script for claiming the input (non-Segwit/non-Taproot). + bytes script_sig = 5; + + // The script for claiming the input (Segit/Taproot). + repeated bytes witness_items = 6; +} + +message TransactionOutput { + // The condition for claiming the output. + bytes script_pubkey = 1; + + // The amount of satoshis to spend. + uint64 value = 2; + + // In case of P2TR script-path (complex scripts), this is the payload that + // must later be revealed and is required for claiming. + bytes taproot_payload = 3; + + // In case of P2TR script-path (complex scripts), this is the control block + // required for claiming. + bytes control_block = 4; +} + +message ComposePlan { + oneof compose { + ComposeBrc20Plan brc20 = 1; + } + + message ComposeBrc20Plan { + // (optional) Sets the private key in the composed transactions. Can + // also be added manually. + bytes private_key = 1; + + // The inputs for the commit transaction. + repeated Input inputs = 2; + + // How the inputs for the commit transaction should be selected. + Utxo.Proto.InputSelector input_selector = 3; + + // The tagged output of the inscription. Commonly a P2WPKH transaction + // with the value of 546 (dust limit). + Output tagged_output = 4; + + // The BRC20 payload to inscribe. + Input.InputBrc20Inscription inscription = 5; + + // The amount of satoshis per vbyte ("satVb"), used for fee calculation. + uint64 fee_per_vb = 6; + + // The change output to be added (return to sender). + // The `value` can be left at 0. + Output change_output = 7; + + // Explicility disable change output creation. + bool disable_change_output = 8; + } +} + +message TransactionPlan { + // A possible error, `OK` if none. + Error error = 1; + + string error_message = 2; + + oneof plan { + Brc20Plan brc20 = 3; + } + + message Brc20Plan { + SigningInput commit = 1; + SigningInput reveal = 2; + } +} diff --git a/src/proto/Utxo.proto b/src/proto/Utxo.proto new file mode 100644 index 00000000000..ada5c6bad9a --- /dev/null +++ b/src/proto/Utxo.proto @@ -0,0 +1,221 @@ +syntax = "proto3"; + +package TW.Utxo.Proto; +option java_package = "wallet.core.jni.proto"; + +enum Error { + OK = 0; + Error_invalid_leaf_hash = 1; + Error_invalid_sighash_type = 2; + Error_invalid_lock_time = 3; + Error_invalid_txid = 4; + Error_sighash_failed = 5; + Error_missing_sighash_method = 6; + Error_failed_encoding = 7; + Error_insufficient_inputs = 8; + Error_missing_change_script_pubkey = 9; +} + +message SigningInput { + // The protocol version. + int32 version = 1; + + // Block height or timestamp indicating at what point transactions can be + // included in a block. + LockTime lock_time = 2; + + // The inputs of the transaction. + repeated TxIn inputs = 3; + + // The outputs of the transaction. + repeated TxOut outputs = 4; + + // How inputs should be selected. + InputSelector input_selector = 5; + + // The base unit per weight. In the case of Bitcoin, that would refer to + // satoshis by vbyte ("satVb"). + uint64 weight_base = 6; + + // The change output where to send left-over funds to (usually the sender). + bytes change_script_pubkey = 7; + + // Explicility disable change output creation. + bool disable_change_output = 8; +} + +enum InputSelector { + // Use all the inputs provided in the given order. + UseAll = 0; + // Automatically select enough inputs in the given order to cover the + // outputs of the transaction. + SelectInOrder = 1; + // Automatically select enough inputs in an ascending order to cover the + // outputs of the transaction. + SelectAscending = 2; +} + +message LockTime { + oneof variant { + uint32 blocks = 1; + uint32 seconds = 2; + } +} + +message TxIn { + // The referenced transaction ID in REVERSED order. + bytes txid = 1; + + // The position in the previous transactions output that this input + // references. + uint32 vout = 2; + + // The value of this input, such as satoshis. Required for producing + // Segwit/Taproot transactions. + uint64 value = 3; + + // The sequence number, used for timelocks, replace-by-fee, etc. Normally + // this number is simply 4294967295 (0xFFFFFFFF) . + uint32 sequence = 4; + + // The spending condition of the referenced output. + bytes script_pubkey = 7; + + // The sighash type, normally `SighashType::UseDefault` (All). + SighashType sighash_type = 8; + + // The signing method. + SigningMethod signing_method = 9; + + // The estimated weight of the input, required for estimating fees. + uint64 weight_estimate = 10; + + // If this input is a Taproot script-path (complex transaction), then this + // leaf hash is required in order to compute the sighash. + bytes leaf_hash = 11; +} + +enum SigningMethod { + // Used for P2SH and P2PKH + Legacy = 0; + // Used for P2WSH and P2WPKH + Segwit = 1; + // Used for P2TR key-path and P2TR script-paty + TaprootAll = 2; + // Used for P2TR key-path and P2TR script-paty if only one prevout should be + // used to calculate the Sighash. Normally this is not used. + TaprootOnePrevout = 3; +} + +enum SighashType { + // Use default (All) + UseDefault = 0; // 0x00 + // Sign all outputs (default). + All = 1; // 0x01 + // Sign no outputs, anyone can choose the destination. + None = 2; // 0x02 + // Sign the output whose index matches this inputs index. + Single = 3; // 0x03 + //Sign all outputs but only this input. + AllPlusAnyoneCanPay = 129; // 0x81 + // Sign no outputs and only this input. + NonePlusAnyoneCanPay = 130; // 0x82 + // Sign one output and only this input. + SinglePlusAnyoneCanPay = 131; // 0x83 +} + +// The output of a transaction. +message TxOut { + // The value of the output. + uint64 value = 1; + // The spending condition of the output. + bytes script_pubkey = 2; +} + +message PreSigningOutput { + // error code, 0 is ok, other codes will be treated as errors + Error error = 1; + + // The transaction ID in NON-reversed order. Note that this must be reversed + // when referencing in future transactions. + bytes txid = 2; + + /// Sighashes to be signed; ECDSA for legacy and Segwit, Schnorr for Taproot. + repeated Sighash sighashes = 3; + + // The raw inputs. + repeated TxIn inputs = 4; + + // The raw outputs. + repeated TxOut outputs = 5; + + // The estimated weight of the transaction. + uint64 weight_estimate = 6; + + // The estimated fee of the transaction denominated in the base unit (such + // as satoshis). + uint64 fee_estimate = 7; +} + +message Sighash { + // The sighash to be signed. + bytes sighash = 1; + // The used signing method for this sighash. + SigningMethod signing_method = 2; + // The used sighash type for this sighash. + SighashType sighash_type = 3; +} + +message PreSerialization { + // The protocol version, is currently expected to be 1 or 2 (BIP68) + int32 version = 1; + + // Block height or timestamp indicating at what point transactions can be + // included in a block. + LockTime lock_time = 2; + + // The transaction inputs containing the serialized claim scripts. + repeated TxInClaim inputs = 3; + + // The transaction outputs. + repeated TxOut outputs = 4; + + // The base unit per weight. In the case of Bitcoin, that would refer to + // satoshis ("satVb"). + uint64 weight_base = 5; +} + +message TxInClaim { + // The referenced transaction hash. + bytes txid = 1; + + // The index of the referenced output. + uint32 vout = 2; + + // The sequence number (TODO). + uint32 sequence = 3; + + // The script used for claiming an input. + bytes script_sig = 4; + + // The script used for claiming an input. + repeated bytes witness_items = 5; +} + +message SerializedTransaction { + // error code, 0 is ok, other codes will be treated as errors + Error error = 1; + + // The encoded transaction, ready to be submitted to the network. + bytes encoded = 2; + + // The transaction ID. + bytes txid = 3; + + // The total and final weight of the transaction. + uint64 weight = 4; + + // The total and final fee of the transaction denominated in the base unit + // (such as satoshis). + uint64 fee = 5; +}