diff --git a/Cargo.toml b/Cargo.toml index 249e41a..f333104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ord-rs" categories = ["cryptography::cryptocurrencies"] license = "MIT" -version = "0.2.0" +version = "0.2.1" authors = ["Finity Technologies"] description = "A library for working with Ordinal inscriptions." repository = "https://github.com/bitfinity-network/ord-rs" @@ -10,17 +10,18 @@ documentation = "https://docs.rs/ord-rs" edition = "2021" [features] -default = [] +default = ["rand"] +rand = ["bitcoin/rand", "dep:rand"] rune = ["ordinals"] [dependencies] async-trait = "0.1" -bitcoin = { version = "0.31", features = ["rand", "serde"] } +bitcoin = { version = "0.31", features = ["serde"] } ciborium = "0.2" hex = "0.4" log = "0.4" ordinals = { version = "0.0.9", optional = true } -rand = { version = "0.8" } +rand = { version = "0.8", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_with = { version = "3", default-features = false, features = ["macros"] } @@ -44,26 +45,32 @@ tokio = { version = "1", features = ["full"] } [[example]] name = "generate-address" path = "examples/generate_address.rs" +required-features = ["rand"] [[example]] name = "transfer" path = "examples/transfer.rs" +required-features = ["rand"] [[example]] name = "mint" path = "examples/mint.rs" +required-features = ["rand"] [[example]] name = "deploy" path = "examples/deploy.rs" +required-features = ["rand"] [[example]] name = "send-inscription" path = "examples/send_inscription.rs" +required-features = ["rand"] [[example]] name = "print-script" path = "examples/print_script.rs" +required-features = ["rand"] [[example]] name = "edict" diff --git a/examples/deploy.rs b/examples/deploy.rs index 3d7d75a..cb00695 100644 --- a/examples/deploy.rs +++ b/examples/deploy.rs @@ -5,7 +5,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; use ord_rs::wallet::{ - CreateCommitTransactionArgsV2, RevealTransactionArgs, SignCommitTransactionArgs, + CreateCommitTransactionArgsV2, RevealTransactionArgs, SignCommitTransactionArgs, TaprootKeypair, }; use ord_rs::{Brc20, OrdTransactionBuilder}; use utils::rpc_client; @@ -94,6 +94,7 @@ async fn main() -> anyhow::Result<()> { leftovers_recipient: sender_address.clone(), commit_fee, reveal_fee, + taproot_keypair: Some(TaprootKeypair::Random), }, )?; let signed_commit_tx = builder diff --git a/examples/etch.rs b/examples/etch.rs index c90f93c..80a1a76 100644 --- a/examples/etch.rs +++ b/examples/etch.rs @@ -8,6 +8,7 @@ use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; use ord_rs::wallet::{ CreateCommitTransactionArgsV2, EtchingTransactionArgs, Runestone, SignCommitTransactionArgs, + TaprootKeypair, }; use ord_rs::{Nft, OrdTransactionBuilder}; use ordinals::{Etching, Rune, Terms}; @@ -123,6 +124,7 @@ async fn main() -> anyhow::Result<()> { leftovers_recipient: sender_address.clone(), commit_fee, reveal_fee, + taproot_keypair: Some(TaprootKeypair::Random), }, )?; let signed_commit_tx = builder diff --git a/examples/mint.rs b/examples/mint.rs index 7c39ad9..fb95e22 100644 --- a/examples/mint.rs +++ b/examples/mint.rs @@ -5,7 +5,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; use ord_rs::wallet::{ - CreateCommitTransactionArgsV2, RevealTransactionArgs, SignCommitTransactionArgs, + CreateCommitTransactionArgsV2, RevealTransactionArgs, SignCommitTransactionArgs, TaprootKeypair, }; use ord_rs::{Brc20, OrdTransactionBuilder}; @@ -89,6 +89,7 @@ async fn main() -> anyhow::Result<()> { leftovers_recipient: sender_address.clone(), commit_fee, reveal_fee, + taproot_keypair: Some(TaprootKeypair::Random), }, )?; diff --git a/examples/transfer.rs b/examples/transfer.rs index 909fc6e..85fb9f3 100644 --- a/examples/transfer.rs +++ b/examples/transfer.rs @@ -5,7 +5,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; use ord_rs::wallet::{ - CreateCommitTransactionArgsV2, RevealTransactionArgs, SignCommitTransactionArgs, + CreateCommitTransactionArgsV2, RevealTransactionArgs, SignCommitTransactionArgs, TaprootKeypair, }; use ord_rs::{Brc20, OrdTransactionBuilder}; @@ -88,6 +88,7 @@ async fn main() -> anyhow::Result<()> { leftovers_recipient: sender_address.clone(), commit_fee, reveal_fee, + taproot_keypair: Some(TaprootKeypair::Random), }, )?; debug!("commit transaction: {commit_tx:?}"); diff --git a/src/error.rs b/src/error.rs index d36edf6..b11aeb0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use thiserror::Error; /// Ordinal transaction handling error types #[derive(Error, Debug)] pub enum OrdError { + #[error("when using P2TR, the taproot keypair option must be provided")] + TaprootKeypairNotProvided, #[error("Hex codec error: {0}")] HexCodec(#[from] hex::FromHexError), #[error("Ord codec error: {0}")] diff --git a/src/wallet.rs b/src/wallet.rs index cbb6277..980641c 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -7,7 +7,7 @@ pub(crate) use builder::RUNE_POSTAGE; pub use builder::{ CreateCommitTransaction, CreateCommitTransactionArgs, CreateCommitTransactionArgsV2, OrdTransactionBuilder, RedeemScriptPubkey, RevealTransactionArgs, ScriptType, - SignCommitTransactionArgs, TaprootPayload, TxInputInfo, Utxo, + SignCommitTransactionArgs, TaprootKeypair, TaprootPayload, TxInputInfo, Utxo, }; #[cfg(feature = "rune")] pub use builder::{CreateEdictTxArgs, EtchingTransactionArgs, Runestone}; diff --git a/src/wallet/builder.rs b/src/wallet/builder.rs index ddbfa72..71f6a14 100644 --- a/src/wallet/builder.rs +++ b/src/wallet/builder.rs @@ -1,3 +1,6 @@ +pub mod signer; +mod taproot; + use bitcoin::absolute::LockTime; use bitcoin::bip32::DerivationPath; use bitcoin::script::{Builder as ScriptBuilder, PushBytesBuf}; @@ -6,10 +9,9 @@ use bitcoin::{ secp256k1, Address, Amount, FeeRate, Network, OutPoint, PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey, }; -use signer::Wallet; -use self::taproot::generate_keypair; -pub use self::taproot::TaprootPayload; +use self::signer::Wallet; +pub use self::taproot::{TaprootKeypair, TaprootPayload}; use crate::inscription::Inscription; use crate::utils::constants::POSTAGE; use crate::utils::fees::{estimate_commit_fee, estimate_reveal_fee, MultisigConfig}; @@ -23,9 +25,6 @@ pub use rune::{CreateEdictTxArgs, EtchingTransactionArgs, Runestone, RUNE_POSTAG use crate::wallet::builder::signer::LocalSigner; -pub mod signer; -mod taproot; - /// Ordinal-aware transaction builder for arbitrary (`Nft`) /// and `Brc20` inscriptions. pub struct OrdTransactionBuilder { @@ -36,6 +35,29 @@ pub struct OrdTransactionBuilder { signer: Wallet, } +/// Unspent transaction output to be used as input of a transaction +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Utxo { + pub id: Txid, + pub index: u32, + pub amount: Amount, +} + +/// Output of a previous transaction to be used as an input. +/// +/// This struct contains signature script in contrast to [Utxo] so it can be used to sign inputs +/// from different addresses. +#[derive(Debug, Clone)] +pub struct TxInputInfo { + /// ID of the output. + pub outpoint: OutPoint, + + /// Contents of the output. + pub tx_out: TxOut, + + pub derivation_path: DerivationPath, +} + #[derive(Debug)] /// Arguments for creating a commit transaction pub struct CreateCommitTransactionArgs @@ -54,6 +76,30 @@ where pub fee_rate: FeeRate, /// Multisig configuration, if applicable pub multisig_config: Option, + /// Optional taproot keypair + pub taproot_keypair: Option, +} + +#[derive(Debug)] +/// Arguments for creating a commit transaction with fixed fees +pub struct CreateCommitTransactionArgsV2 +where + T: Inscription, +{ + /// UTXOs to be used as inputs of the transaction + pub inputs: Vec, + /// Inscription to write + pub inscription: T, + /// Address to send the leftovers BTC of the trasnsaction + pub leftovers_recipient: Address, + /// Fee to pay for the commit transaction + pub commit_fee: Amount, + /// Fee to pay for the reveal transaction + pub reveal_fee: Amount, + /// Script pubkey of the inputs + pub txin_script_pubkey: ScriptBuf, + /// Optional taproot keypair + pub taproot_keypair: Option, } #[derive(Debug, Clone)] @@ -161,7 +207,12 @@ impl OrdTransactionBuilder { // generate P2TR keyts let p2tr_keys = match self.script_type { ScriptType::P2WSH => None, - ScriptType::P2TR => Some(generate_keypair(&secp_ctx)), + ScriptType::P2TR => { + let taproot_keypair = args + .taproot_keypair + .ok_or(OrdError::TaprootKeypairNotProvided)?; + Some(taproot_keypair.generate_keypair(&secp_ctx)) + } }; // generate redeem script pubkey based on the current script type @@ -403,7 +454,12 @@ impl OrdTransactionBuilder { // generate P2TR keyts let p2tr_keys = match self.script_type { ScriptType::P2WSH => None, - ScriptType::P2TR => Some(generate_keypair(&secp_ctx)), + ScriptType::P2TR => { + let taproot_keypair = args + .taproot_keypair + .ok_or(OrdError::TaprootKeypairNotProvided)?; + Some(taproot_keypair.generate_keypair(&secp_ctx)) + } }; // generate redeem script pubkey based on the current script type @@ -499,49 +555,6 @@ impl OrdTransactionBuilder { } } -#[derive(Debug)] -/// Arguments for creating a commit transaction -pub struct CreateCommitTransactionArgsV2 -where - T: Inscription, -{ - /// UTXOs to be used as inputs of the transaction - pub inputs: Vec, - /// Inscription to write - pub inscription: T, - /// Address to send the leftovers BTC of the trasnsaction - pub leftovers_recipient: Address, - /// Fee to pay for the commit transaction - pub commit_fee: Amount, - /// Fee to pay for the reveal transaction - pub reveal_fee: Amount, - /// Script pubkey of the inputs - pub txin_script_pubkey: ScriptBuf, -} - -/// Unspent transaction output to be used as input of a transaction -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Utxo { - pub id: Txid, - pub index: u32, - pub amount: Amount, -} - -/// Output of a previous transaction to be used as an input. -/// -/// This struct contains signature script in contrast to [Utxo] so it can be used to sign inputs -/// from different addresses. -#[derive(Debug, Clone)] -pub struct TxInputInfo { - /// ID of the output. - pub outpoint: OutPoint, - - /// Contents of the output. - pub tx_out: TxOut, - - pub derivation_path: DerivationPath, -} - #[cfg(test)] mod test { use std::str::FromStr; @@ -557,6 +570,7 @@ mod test { const WIF: &str = "cVkWbHmoCx6jS8AyPNQqvFr8V9r2qzDHJLaxGDQgDJfxT73w6fuU"; #[tokio::test] + #[cfg(feature = "rand")] async fn test_should_build_transfer_for_brc20_transactions_from_existing_data_with_p2wsh() { // this test refers to these testnet transactions, commit and reveal: // @@ -581,6 +595,7 @@ mod test { leftovers_recipient: address.clone(), commit_fee: Amount::from_sat(2_500), reveal_fee: Amount::from_sat(4_700), + taproot_keypair: Some(TaprootKeypair::Random), }; let tx_result = builder .build_commit_transaction_with_fixed_fees(Network::Testnet, commit_transaction_args) @@ -666,6 +681,7 @@ mod test { } #[tokio::test] + #[cfg(feature = "rand")] async fn test_should_build_transfer_for_brc20_transactions_from_existing_data_with_p2tr() { // this test refers to these testnet transactions, commit and reveal: // @@ -690,6 +706,7 @@ mod test { leftovers_recipient: address.clone(), commit_fee: Amount::from_sat(2_500), reveal_fee: Amount::from_sat(4_700), + taproot_keypair: Some(TaprootKeypair::Random), }; let tx_result = builder .build_commit_transaction_with_fixed_fees(Network::Testnet, commit_transaction_args) diff --git a/src/wallet/builder/rune.rs b/src/wallet/builder/rune.rs index ee85fb3..22ef241 100644 --- a/src/wallet/builder/rune.rs +++ b/src/wallet/builder/rune.rs @@ -247,7 +247,7 @@ mod tests { use hex_literal::hex; use super::*; - use crate::wallet::{CreateCommitTransactionArgsV2, LocalSigner}; + use crate::wallet::{CreateCommitTransactionArgsV2, LocalSigner, TaprootKeypair}; use crate::{Nft, SignCommitTransactionArgs, Wallet}; // @@ -383,6 +383,7 @@ mod tests { } #[tokio::test] + #[cfg(feature = "rand")] async fn test_should_append_runestone() { // this test refers to these testnet transactions, commit and reveal: // @@ -412,6 +413,7 @@ mod tests { leftovers_recipient: address.clone(), commit_fee: Amount::from_sat(2_500), reveal_fee: Amount::from_sat(4_700), + taproot_keypair: Some(TaprootKeypair::Random), }; let tx_result = builder .build_commit_transaction_with_fixed_fees(Network::Testnet, commit_transaction_args) diff --git a/src/wallet/builder/taproot.rs b/src/wallet/builder/taproot.rs index 74633b5..fef3e70 100644 --- a/src/wallet/builder/taproot.rs +++ b/src/wallet/builder/taproot.rs @@ -1,8 +1,11 @@ +mod taproot_keypair; + use bitcoin::key::UntweakedKeypair; use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::taproot::{ControlBlock, LeafVersion, TaprootBuilder}; use bitcoin::{Address, Amount, Network, ScriptBuf, TxOut, XOnlyPublicKey}; +pub use self::taproot_keypair::TaprootKeypair; use crate::{OrdError, OrdResult}; #[derive(Debug, Clone)] @@ -45,9 +48,3 @@ impl TaprootPayload { }) } } - -pub fn generate_keypair(secp: &Secp256k1) -> (UntweakedKeypair, XOnlyPublicKey) { - let keypair = UntweakedKeypair::new(secp, &mut rand::thread_rng()); - let x_public_key = XOnlyPublicKey::from_keypair(&keypair).0; - (keypair, x_public_key) -} diff --git a/src/wallet/builder/taproot/taproot_keypair.rs b/src/wallet/builder/taproot/taproot_keypair.rs new file mode 100644 index 0000000..614545e --- /dev/null +++ b/src/wallet/builder/taproot/taproot_keypair.rs @@ -0,0 +1,25 @@ +use bitcoin::key::{Keypair, Secp256k1}; +use bitcoin::secp256k1::{All, SecretKey}; +use bitcoin::XOnlyPublicKey; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TaprootKeypair { + /// Generate a keypair using a secret key + SecretKey(SecretKey), + /// Generate a keypair using a random number generator + #[cfg(feature = "rand")] + Random, +} + +impl TaprootKeypair { + pub fn generate_keypair(&self, secp: &Secp256k1) -> (Keypair, XOnlyPublicKey) { + let keypair = match self { + Self::SecretKey(secret_key) => Keypair::from_secret_key(secp, secret_key), + #[cfg(feature = "rand")] + Self::Random => Keypair::new(secp, &mut rand::thread_rng()), + }; + + let x_public_key = XOnlyPublicKey::from_keypair(&keypair).0; + (keypair, x_public_key) + } +}