diff --git a/.vscode/settings.json b/.vscode/settings.json index 49add19b..5827acd4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,8 +5,8 @@ "files.autoSave": "onFocusChange", "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, - "rust-analyzer.cargo.features": "all", // Enable only for desktop "rust-analyzer.check.allTargets": true, + "rust-analyzer.cargo.features": "all", // Enable only for desktop // "rust-analyzer.cargo.target": "wasm32-unknown-unknown", // Enable only for web // "rust-analyzer.check.noDefaultFeatures": true, // Enable for web // "rust-analyzer.runnables.extraArgs": ["--release"], // Enable for web diff --git a/lib/web/bitcoin.ts b/lib/web/bitcoin.ts index 2fc3f892..21401d9a 100644 --- a/lib/web/bitcoin.ts +++ b/lib/web/bitcoin.ts @@ -57,16 +57,6 @@ export const sendSats = async ( await BMC.send_sats(descriptor, changeDescriptor, address, amount, feeRate) ); -export const drainWallet = async ( - destination: string, - descriptor: string, - changeDescriptor?: string, - feeRate?: number -): Promise => - JSON.parse( - await BMC.drain_wallet(destination, descriptor, changeDescriptor, feeRate) - ); - export const fundVault = async ( descriptor: string, changeDescriptor: string, @@ -92,6 +82,27 @@ export const getAssetsVault = async ( await BMC.get_assets_vault(rgbAssetsDescriptorXpub, rgbUdasDescriptorXpub) ); +export const drainWallet = async ( + destination: string, + descriptor: string, + changeDescriptor?: string, + feeRate?: number +): Promise => + JSON.parse( + await BMC.drain_wallet(destination, descriptor, changeDescriptor, feeRate) + ); + +export const bumpFee = async ( + txid: string, + feeRate: number, + broadcast: boolean, + descriptor: string, + changeDescriptor?: string, +): Promise => + JSON.parse( + await BMC.bump_fee(txid, feeRate, descriptor, changeDescriptor, broadcast) + ); + // Core type interfaces based on structs defined within the bitmask-core Rust crate: // https://github.com/diba-io/bitmask-core/blob/development/src/structs.rs @@ -123,7 +134,7 @@ export interface Vault { public: PublicWalletData; } -export interface Transaction { +export interface Transaction extends WalletTransaction { amount: number; asset?: string; assetType: string; @@ -162,6 +173,12 @@ export interface TransactionDetails extends Transaction { } export interface TransactionData { + details: TransactionDataDetails; + vsize: number; + feeRate: number; +} + +export interface TransactionDataDetails { transaction?: Transaction; txid: string; received: number; @@ -183,6 +200,8 @@ export interface WalletTransaction { fee: number; confirmed: boolean; confirmationTime: ConfirmationTime; + vsize: number; + feeRate: number; } export interface WalletBalance { diff --git a/lib/web/package-lock.json b/lib/web/package-lock.json index 88fa2a3f..bcacd74c 100644 --- a/lib/web/package-lock.json +++ b/lib/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitmask-core", - "version": "0.7.0-beta.8", + "version": "0.7.0-beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bitmask-core", - "version": "0.7.0-beta.8", + "version": "0.7.0-beta.10", "license": "MIT", "devDependencies": { "@types/node": "^20.8.2", diff --git a/lib/web/package.json b/lib/web/package.json index fecd44f0..d6d90191 100644 --- a/lib/web/package.json +++ b/lib/web/package.json @@ -6,7 +6,7 @@ "Francisco Calderón " ], "description": "Core functionality for the BitMask wallet", - "version": "0.7.0-beta.8", + "version": "0.7.0-beta.10", "license": "MIT", "repository": { "type": "git", diff --git a/lib/web/rgb.ts b/lib/web/rgb.ts index c9a539c4..f41c82cb 100644 --- a/lib/web/rgb.ts +++ b/lib/web/rgb.ts @@ -819,10 +819,10 @@ export interface RgbOfferRequest { expireAt?: number; } -export interface RgbSwapStrategy { - auction?: string, - p2p?: string, - hotswap?: string, +export enum RgbSwapStrategy { + Auction = "auction", + P2P = "p2p", + HotSwap = "hotswap", } export interface RgbAuctionStrategy { diff --git a/src/bitcoin.rs b/src/bitcoin.rs index 1b0c955a..7843fe9f 100644 --- a/src/bitcoin.rs +++ b/src/bitcoin.rs @@ -5,7 +5,7 @@ use ::psbt::Psbt; use amplify::hex::ToHex; use argon2::Argon2; use bdk::{wallet::AddressIndex, FeeRate, LocalUtxo, SignOptions, TransactionDetails}; -use bitcoin::{consensus::encode, psbt::PartiallySignedTransaction}; +use bitcoin::{consensus::encode, psbt::PartiallySignedTransaction, Txid}; use rand::{rngs::StdRng, Rng, SeedableRng}; use serde_encrypt::{ serialize::impls::BincodeSerializer, shared_key::SharedKey, traits::SerdeEncryptSharedKey, @@ -40,7 +40,7 @@ use crate::{ structs::{ DecryptedWalletData, EncryptedWalletDataV04, FundVaultDetails, PublishPsbtRequest, PublishedPsbtResponse, SatsInvoice, SecretString, SignPsbtRequest, SignedPsbtResponse, - WalletData, WalletTransaction, + TransactionData, WalletData, WalletTransaction, }, trace, }; @@ -113,6 +113,9 @@ pub enum BitcoinError { /// PSBT decode error #[error(transparent)] BitcoinPsbtDecodeError(#[from] bitcoin::consensus::encode::Error), + /// Txid parse error + #[error(transparent)] + TxidParseError(#[from] bitcoin::hashes::hex::Error), } /// Bitcoin Wallet Operations @@ -270,7 +273,7 @@ pub async fn get_wallet_data( let mut transactions = wallet .lock() .await - .list_transactions(false) + .list_transactions(true) .unwrap_or_default(); trace!(format!("transactions: {transactions:#?}")); @@ -283,13 +286,23 @@ pub async fn get_wallet_data( let transactions: Vec = transactions .into_iter() - .map(|tx| WalletTransaction { - txid: tx.txid, - received: tx.received, - sent: tx.sent, - fee: tx.fee, - confirmed: tx.confirmation_time.is_some(), - confirmation_time: tx.confirmation_time, + .map(|tx| { + let vsize = match &tx.transaction { + Some(tx_details) => tx_details.vsize(), + None => 1, + }; + let fee_rate = tx.fee.expect("tx fee exists") as f32 / vsize as f32; + + WalletTransaction { + txid: tx.txid, + received: tx.received, + sent: tx.sent, + fee: tx.fee, + confirmed: tx.confirmation_time.is_some(), + confirmation_time: tx.confirmation_time, + vsize, + fee_rate, + } }) .collect(); @@ -341,13 +354,13 @@ pub async fn send_sats( destination: &str, // bip21 uri or address amount: u64, fee_rate: Option, -) -> Result { +) -> Result { use payjoin::UriExt; let wallet = get_wallet(descriptor, Some(change_descriptor)).await?; let fee_rate = fee_rate.map(FeeRate::from_sat_per_vb); - let transaction = match payjoin::Uri::try_from(destination) { + let details = match payjoin::Uri::try_from(destination) { Ok(uri) => { let address = uri.address.clone(); validate_address(&address).await?; @@ -370,7 +383,17 @@ pub async fn send_sats( } }; - Ok(transaction) + let vsize = match &details.transaction { + Some(tx_details) => tx_details.vsize(), + None => 1, + }; + let fee_rate = details.fee.expect("fee is present on tx") as f32 / vsize as f32; + + Ok(TransactionData { + details, + vsize, + fee_rate, + }) } pub async fn fund_vault( @@ -537,7 +560,7 @@ pub async fn drain_wallet( descriptor: &SecretString, change_descriptor: Option<&SecretString>, fee_rate: Option, -) -> Result { +) -> Result { let address = Address::from_str(destination)?; validate_address(&address).await?; debug!(format!("Create drain wallet tx to: {address:#?}")); @@ -593,8 +616,72 @@ pub async fn drain_wallet( "Drain wallet transaction submitted with details: {details:#?}" )); - Ok(details) + let vsize = match &details.transaction { + Some(tx_details) => tx_details.vsize(), + None => 1, + }; + let fee_rate = details.fee.expect("fee is present on tx") as f32 / vsize as f32; + + Ok(TransactionData { + details, + vsize, + fee_rate, + }) } else { Err(BitcoinError::DrainWalletNoTxDetails) } } + +pub async fn bump_fee( + txid: String, + fee_rate: f32, + descriptor: &SecretString, + change_descriptor: Option<&SecretString>, + broadcast: bool, +) -> Result { + let txid = Txid::from_str(&txid)?; + + let wallet = get_wallet(descriptor, change_descriptor).await?; + + if broadcast { + sync_wallet(&wallet).await?; + } + + let (mut psbt, details) = { + let wallet_lock = wallet.lock().await; + let mut builder = wallet_lock.build_fee_bump(txid)?; + builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate)); + builder.finish()? + }; + + let _finalized = wallet + .lock() + .await + .sign(&mut psbt, SignOptions::default())?; + let tx = psbt.extract_tx(); + + if broadcast { + let blockchain = get_blockchain().await; + blockchain.broadcast(&tx).await?; + } + + let sent = tx.output.iter().fold(0, |sum, output| output.value + sum); + + let txid = tx.txid(); + let vsize = tx.vsize(); + + let details = TransactionDetails { + txid, + transaction: Some(tx), + received: 0, + sent, + fee: details.fee, + confirmation_time: None, + }; + + Ok(TransactionData { + details, + vsize, + fee_rate, + }) +} diff --git a/src/structs.rs b/src/structs.rs index 82851338..3d4b8325 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -31,7 +31,7 @@ pub struct WalletData { pub utxos: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct WalletTransaction { pub txid: Txid, @@ -40,6 +40,16 @@ pub struct WalletTransaction { pub fee: Option, pub confirmed: bool, pub confirmation_time: Option, + pub vsize: usize, + pub fee_rate: f32, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TransactionData { + pub details: TransactionDetails, + pub vsize: usize, + pub fee_rate: f32, } #[derive(Serialize, Deserialize, Clone, Debug, Zeroize, ZeroizeOnDrop, Display, Default)] diff --git a/src/web.rs b/src/web.rs index bf0f2f81..c4c2f9ee 100644 --- a/src/web.rs +++ b/src/web.rs @@ -295,21 +295,21 @@ pub mod bitcoin { } #[wasm_bindgen] - pub fn drain_wallet( - destination: String, + pub fn fund_vault( descriptor: String, - change_descriptor: Option, + change_descriptor: String, + asset_address: String, + uda_address: String, fee_rate: Option, ) -> Promise { set_panic_hook(); future_to_promise(async move { - let change_descriptor = change_descriptor.map(SecretString); - - match crate::bitcoin::drain_wallet( - &destination, + match crate::bitcoin::fund_vault( &SecretString(descriptor), - change_descriptor.as_ref(), + &SecretString(change_descriptor), + &asset_address, + &uda_address, fee_rate, ) .await @@ -323,21 +323,43 @@ pub mod bitcoin { } #[wasm_bindgen] - pub fn fund_vault( + pub fn get_assets_vault( + rgb_assets_descriptor_xpub: String, + rgb_udas_descriptor_xpub: String, + ) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::bitcoin::get_assets_vault( + &SecretString(rgb_assets_descriptor_xpub), + &SecretString(rgb_udas_descriptor_xpub), + ) + .await + { + Ok(result) => Ok(JsValue::from_string( + serde_json::to_string(&result).unwrap(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } + + #[wasm_bindgen] + pub fn drain_wallet( + destination: String, descriptor: String, - change_descriptor: String, - asset_address_1: String, - uda_address_1: String, + change_descriptor: Option, fee_rate: Option, ) -> Promise { set_panic_hook(); future_to_promise(async move { - match crate::bitcoin::fund_vault( + let change_descriptor = change_descriptor.map(SecretString); + + match crate::bitcoin::drain_wallet( + &destination, &SecretString(descriptor), - &SecretString(change_descriptor), - &asset_address_1, - &uda_address_1, + change_descriptor.as_ref(), fee_rate, ) .await @@ -351,16 +373,23 @@ pub mod bitcoin { } #[wasm_bindgen] - pub fn get_assets_vault( - rgb_assets_descriptor_xpub: String, - rgb_udas_descriptor_xpub: String, + pub fn bump_fee( + txid: String, + fee_rate: f32, + descriptor: String, + change_descriptor: Option, + broadcast: bool, ) -> Promise { set_panic_hook(); future_to_promise(async move { - match crate::bitcoin::get_assets_vault( - &SecretString(rgb_assets_descriptor_xpub), - &SecretString(rgb_udas_descriptor_xpub), + let change_descriptor = change_descriptor.map(SecretString); + match crate::bitcoin::bump_fee( + txid, + fee_rate, + &SecretString(descriptor), + change_descriptor.as_ref(), + broadcast, ) .await { diff --git a/tests/rgb/integration/drain.rs b/tests/rgb/integration/drain.rs index 305c37ac..b5626197 100644 --- a/tests/rgb/integration/drain.rs +++ b/tests/rgb/integration/drain.rs @@ -49,11 +49,11 @@ pub async fn drain() -> Result<()> { .await?; assert_eq!( - drain_wallet_details.received, 0, + drain_wallet_details.details.received, 0, "received no funds in this transaction" ); assert_eq!( - drain_wallet_details.sent + drain_wallet_details.fee.expect("fee present"), + drain_wallet_details.details.sent + drain_wallet_details.details.fee.expect("fee present"), 30_000_000, "received 0.3 tBTC" ); diff --git a/tests/rgb/integration/rbf.rs b/tests/rgb/integration/rbf.rs index d8328cfe..9665631a 100644 --- a/tests/rgb/integration/rbf.rs +++ b/tests/rgb/integration/rbf.rs @@ -1,6 +1,4 @@ #![cfg(not(target_arch = "wasm32"))] -use std::str::FromStr; - use anyhow::Result; use bdk::{ database::MemoryDatabase, @@ -10,10 +8,11 @@ use bdk::{ }; use bitcoin::{secp256k1::Secp256k1, Network, Txid}; use bitmask_core::{ - bitcoin::{get_blockchain, new_mnemonic, sign_and_publish_psbt_file}, + bitcoin::{bump_fee, get_blockchain, new_mnemonic, sign_and_publish_psbt_file}, rgb::{get_contract, structs::ContractAmount}, structs::{PsbtFeeRequest, PsbtResponse, SecretString, SignPsbtRequest}, }; +use std::str::FromStr; use crate::rgb::integration::utils::{ create_new_psbt_v2, issuer_issue_contract_v2, send_some_coins, UtxoFilter, @@ -131,7 +130,6 @@ pub async fn create_simple_rbf_bitcoin_transfer() -> Result<()> { Ok(()) } -#[ignore = "No longer necessary, this is a simple test to rbf with bdk"] #[tokio::test] pub async fn create_bdk_rbf_transaction() -> Result<()> { // 1. Initial Setup @@ -149,6 +147,7 @@ pub async fn create_bdk_rbf_transaction() -> Result<()> { let user_address = user_wallet_data.get_address(AddressIndex::New)?; send_some_coins(&user_address.address.to_string(), "1").await; + // 2. Send sats user_wallet_data .sync(&blockchain, SyncOptions::default()) .await?; @@ -164,10 +163,8 @@ pub async fn create_bdk_rbf_transaction() -> Result<()> { let (mut psbt, _) = builder.finish()?; let _ = user_wallet_data.sign(&mut psbt, SignOptions::default())?; - // println!("{:#?}", signed); let tx = psbt.extract_tx(); - blockchain.broadcast(&tx).await?; user_wallet_data @@ -175,8 +172,6 @@ pub async fn create_bdk_rbf_transaction() -> Result<()> { .await?; let txs = user_wallet_data.list_transactions(false)?; - // println!("{:#?}", txs); - assert_eq!(2, txs.len()); let tx_1_utxos: Vec = tx @@ -186,24 +181,20 @@ pub async fn create_bdk_rbf_transaction() -> Result<()> { .map(|u| u.previous_output.to_string()) .collect(); - let (mut psbt, ..) = { - let mut builder = user_wallet_data.build_fee_bump(tx.txid())?; - builder.fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)); - builder.finish()? - }; - - let _ = user_wallet_data.sign(&mut psbt, SignOptions::default())?; - let tx = psbt.extract_tx(); - let blockchain = get_blockchain().await; - blockchain.broadcast(&tx).await?; + bump_fee( + tx.txid().to_string(), + 5.0, + &SecretString(user_keys.private.btc_descriptor_xprv.to_owned()), + None, + true, + ) + .await?; user_wallet_data .sync(&blockchain, SyncOptions::default()) .await?; let txs = user_wallet_data.list_transactions(false)?; - // println!("{:#?}", txs); - assert_eq!(2, txs.len()); let tx_2_utxos: Vec = tx @@ -212,8 +203,8 @@ pub async fn create_bdk_rbf_transaction() -> Result<()> { .map(|u| u.previous_output.to_string()) .collect(); - // println!("{:#?}", tx_1_utxos); - // println!("{:#?}", tx_2_utxos); + // println!("tx 1 utxos: {:#?}", tx_1_utxos); + // println!("tx 2 utxos: {:#?}", tx_2_utxos); assert_eq!(tx_1_utxos, tx_2_utxos); Ok(()) diff --git a/tests/web_wallet.rs b/tests/web_wallet.rs index 48d4a02c..06ef5c0d 100644 --- a/tests/web_wallet.rs +++ b/tests/web_wallet.rs @@ -1,7 +1,7 @@ #![cfg(target_arch = "wasm32")] use bitmask_core::{ debug, info, - structs::{DecryptedWalletData, SecretString, TransactionDetails, WalletData}, + structs::{DecryptedWalletData, SecretString, TransactionData, WalletData}, web::{ bitcoin::{ decrypt_wallet, encrypt_wallet, get_wallet_data, hash_password, new_wallet, send_sats, @@ -186,10 +186,10 @@ async fn import_test_wallet() { .await; info!("Parse tx_details"); - let tx_data: TransactionDetails = json_parse(&tx_details); + let tx_data: TransactionData = json_parse(&tx_details); assert!( - tx_data.confirmation_time.is_none(), + tx_data.details.confirmation_time.is_none(), "latest transaction hasn't been confirmed yet" ); }