diff --git a/clients/kotlin/src/main/kotlin/TransferSend.kt b/clients/kotlin/src/main/kotlin/TransferSend.kt index f472658b..a56bb07d 100644 --- a/clients/kotlin/src/main/kotlin/TransferSend.kt +++ b/clients/kotlin/src/main/kotlin/TransferSend.kt @@ -144,8 +144,6 @@ class TransferSend: CliktCommand(help = "Send the specified coin to an statechai blindingFactor = coin.blindingFactor ?: throw Exception("blindingFactor is null") ) - coin.locktime = getBlockheight(backupTx); - backupTxs = backupTxs.plus(backupTx) val inputTxid = coin.utxoTxid!! diff --git a/clients/nodejs/broadcast_backup_tx.js b/clients/nodejs/broadcast_backup_tx.js index 96a661cd..1102c3da 100644 --- a/clients/nodejs/broadcast_backup_tx.js +++ b/clients/nodejs/broadcast_backup_tx.js @@ -18,17 +18,7 @@ const execute = async (electrumClient, db, walletName, statechainId, toAddress, feeRate = parseInt(feeRate, 10); } - // console.log("feeRate: ", feeRate); - let backupTxs = await sqlite_manager.getBackupTxs(db, statechainId); - - const backupTx = backupTxs.length === 0 ? null : backupTxs.reduce((prev, current) => (prev.tx_n > current.tx_n) ? prev : current); - - if (!backupTx) { - throw new Error(`There is no backup transaction for the statechain id ${statechainId}`); - } - - // console.log("backupTx", backupTx); let coinsWithStatechainId = wallet.coins.filter(c => { return c.statechain_id === statechainId @@ -43,8 +33,14 @@ const execute = async (electrumClient, db, walletName, statechainId, toAddress, // Sort the coins by locktime in ascending order and pick the first one let coin = coinsWithStatechainId.sort((a, b) => a.locktime - b.locktime)[0]; - if (coin.status != CoinStatus.CONFIRMED) { - throw new Error(`Coin status must be CONFIRMED to broadcast the backup transaction. The current status is ${coin.status}`); + if (coin.status != CoinStatus.CONFIRMED && coin.status != CoinStatus.IN_TRANSFER) { + throw new Error(`Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is ${coin.status}`); + } + + const backupTx = mercury_wasm.latestBackuptxPaysToUserpubkey(backupTxs, coin, wallet.network); + + if (!backupTx) { + throw new Error(`There is no backup transaction for the statechain id ${statechainId}`); } const CpfpTx = mercury_wasm.createCpfpTx(backupTx, coin, toAddress, feeRate, wallet.network); diff --git a/clients/nodejs/transfer_send.js b/clients/nodejs/transfer_send.js index 655303a8..02e6fca3 100644 --- a/clients/nodejs/transfer_send.js +++ b/clients/nodejs/transfer_send.js @@ -75,8 +75,6 @@ const execute = async (electrumClient, db, walletName, statechainId, toAddress, blinding_factor: coin.blinding_factor }; - coin.locktime = mercury_wasm.getBlockheight(backup_tx); - backupTxs.push(backup_tx); const input_txid = coin.utxo_txid; diff --git a/clients/nodejs/withdraw.js b/clients/nodejs/withdraw.js index ae458571..a7cb4aea 100644 --- a/clients/nodejs/withdraw.js +++ b/clients/nodejs/withdraw.js @@ -36,8 +36,8 @@ const execute = async (electrumClient, db, walletName, statechainId, toAddress, // Sort the coins by locktime in ascending order and pick the first one let coin = coinsWithStatechainId.sort((a, b) => a.locktime - b.locktime)[0]; - if (coin.status != CoinStatus.CONFIRMED) { - throw new Error(`Coin status must be CONFIRMED to withdraw it. The current status is ${coin.status}`); + if (coin.status != CoinStatus.CONFIRMED && coin.status != CoinStatus.IN_TRANSFER) { + throw new Error(`Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is ${coin.status}`); } const isWithdrawal = true; diff --git a/clients/rust/src/broadcast_backup_tx.rs b/clients/rust/src/broadcast_backup_tx.rs index 678e48da..1e1275f1 100644 --- a/clients/rust/src/broadcast_backup_tx.rs +++ b/clients/rust/src/broadcast_backup_tx.rs @@ -15,14 +15,6 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str, statechain let backup_txs = get_backup_txs(&client_config.pool, &statechain_id).await?; - let backup_tx = backup_txs.iter().max_by_key(|tx| tx.tx_n); - - if backup_tx.is_none() { - return Err(anyhow!("No backup transaction associated with this statechain ID were found")); - } - - let backup_tx = backup_tx.unwrap(); - // If the user sends to himself, he will have two coins with same statechain_id // In this case, we need to find the one with the lowest locktime let coin = wallet.coins @@ -36,10 +28,12 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str, statechain let coin = coin.unwrap(); - if coin.status != CoinStatus::CONFIRMED { - return Err(anyhow::anyhow!("Coin status must be CONFIRMED to broadcast the backup transaction. The current status is {}", coin.status)); + if coin.status != CoinStatus::CONFIRMED && coin.status != CoinStatus::IN_TRANSFER { + return Err(anyhow::anyhow!("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is {}", coin.status)); } + let backup_tx = cpfp_tx::latest_backup_tx_pays_to_user_pubkey(&backup_txs, &coin, &wallet.network)?; + let fee_rate = match fee_rate { Some(fee_rate) => fee_rate, None => { diff --git a/clients/rust/src/transfer_sender.rs b/clients/rust/src/transfer_sender.rs index 453c37c0..be15de60 100644 --- a/clients/rust/src/transfer_sender.rs +++ b/clients/rust/src/transfer_sender.rs @@ -37,7 +37,7 @@ pub async fn execute( let coin = wallet.coins .iter_mut() .filter(|tx| tx.statechain_id == Some(statechain_id.to_string())) // Filter coins with the specified statechain_id - .min_by_key(|tx| tx.locktime); // Find the one with the lowest locktime + .min_by_key(|tx| tx.locktime.unwrap_or(u32::MAX)); // Find the one with the lowest locktime if coin.is_none() { return Err(anyhow!("No coins associated with this statechain ID were found")); @@ -84,8 +84,6 @@ pub async fn execute( blinding_factor: coin.blinding_factor.as_ref().unwrap().to_string(), }; - coin.locktime = Some(get_blockheight(&backup_tx)?); - backup_transactions.push(backup_tx); let input_txid = coin.utxo_txid.as_ref().unwrap(); diff --git a/lib/src/wallet/cpfp_tx.rs b/lib/src/wallet/cpfp_tx.rs index 8e136397..0da61f78 100644 --- a/lib/src/wallet/cpfp_tx.rs +++ b/lib/src/wallet/cpfp_tx.rs @@ -6,6 +6,42 @@ use super::{BackupTx, Coin}; use bitcoin::{Transaction, Address, TxOut, Txid, OutPoint, TxIn, ScriptBuf, Witness, absolute, psbt::{Psbt, Input, PsbtSighashType, self}, bip32::{Fingerprint, DerivationPath}, Amount, Network, sighash::{TapSighashType, SighashCache, self, TapSighash}, taproot::{TapLeafHash, self}, secp256k1, key::TapTweak, PrivateKey}; use secp256k1_zkp::{Secp256k1, SecretKey, PublicKey, XOnlyPublicKey}; +#[cfg_attr(feature = "bindings", uniffi::export)] +pub fn latest_backup_tx_pays_to_user_pubkey(backup_txs: &Vec, coin: &Coin, network: &str) -> Result { + + let network = get_network(network)?; + + let backup_address = Address::from_str(coin.backup_address.as_str())?.require_network(network)?; + + let backup_tx = backup_txs.iter() + .filter_map(|bkp_tx| { + let tx_bytes = hex::decode(&bkp_tx.tx).ok()?; + let tx: Transaction = bitcoin::consensus::deserialize(&tx_bytes).ok()?; + + if tx.output.len() != 1 { + return None; + } + + let output: &TxOut = tx.output.get(0)?; + if backup_address.script_pubkey() == output.script_pubkey { + Some(bkp_tx) + } else { + None + } + }) + .max_by_key(|bkp_tx| bkp_tx.tx_n); + + match backup_tx { + Some(tx) => { + return Ok(tx.clone()); + }, + None => { + return Err(MercuryError::NoBackupTransactionFound); + } + } +} + + #[cfg_attr(feature = "bindings", uniffi::export)] pub fn create_cpfp_tx(backup_tx: &BackupTx, coin: &Coin, to_address: &str, fee_rate_sats_per_byte: u64, network: &str) -> Result { diff --git a/wasm/node_pkg/debug/mercury_wasm.d.ts b/wasm/node_pkg/debug/mercury_wasm.d.ts index eb3be297..3ef8c7d6 100644 --- a/wasm/node_pkg/debug/mercury_wasm.d.ts +++ b/wasm/node_pkg/debug/mercury_wasm.d.ts @@ -251,6 +251,13 @@ export function signMessage(statechain_id: string, coin: any): string; */ export function isEnclavePubkeyPartOfCoin(coin: any, enclave_pubkey: string): boolean; /** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +export function latestBackuptxPaysToUserpubkey(backup_transactions: any, coin: any, network: string): any; +/** * @returns {any} */ export function getMockWallet(): any; diff --git a/wasm/node_pkg/debug/mercury_wasm.js b/wasm/node_pkg/debug/mercury_wasm.js index f78d9803..8b8d7441 100644 --- a/wasm/node_pkg/debug/mercury_wasm.js +++ b/wasm/node_pkg/debug/mercury_wasm.js @@ -819,6 +819,19 @@ module.exports.isEnclavePubkeyPartOfCoin = function(coin, enclave_pubkey) { return ret !== 0; }; +/** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +module.exports.latestBackuptxPaysToUserpubkey = function(backup_transactions, coin, network) { + const ptr0 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.latestBackuptxPaysToUserpubkey(addHeapObject(backup_transactions), addHeapObject(coin), ptr0, len0); + return takeObject(ret); +}; + /** * @returns {any} */ diff --git a/wasm/node_pkg/debug/mercury_wasm_bg.wasm b/wasm/node_pkg/debug/mercury_wasm_bg.wasm index f9ae98ec..27387e63 100644 Binary files a/wasm/node_pkg/debug/mercury_wasm_bg.wasm and b/wasm/node_pkg/debug/mercury_wasm_bg.wasm differ diff --git a/wasm/node_pkg/debug/mercury_wasm_bg.wasm.d.ts b/wasm/node_pkg/debug/mercury_wasm_bg.wasm.d.ts index fe096d2d..b1a3f9fb 100644 --- a/wasm/node_pkg/debug/mercury_wasm_bg.wasm.d.ts +++ b/wasm/node_pkg/debug/mercury_wasm_bg.wasm.d.ts @@ -39,6 +39,7 @@ export function getNewKeyInfo(a: number, b: number, c: number, d: number, e: num export function validateAddress(a: number, b: number, c: number, d: number): number; export function signMessage(a: number, b: number, c: number, d: number): void; export function isEnclavePubkeyPartOfCoin(a: number, b: number, c: number): number; +export function latestBackuptxPaysToUserpubkey(a: number, b: number, c: number, d: number): number; export function getMockWallet(): number; export function rustsecp256k1zkp_v0_8_1_default_illegal_callback_fn(a: number, b: number): void; export function rustsecp256k1zkp_v0_8_1_default_error_callback_fn(a: number, b: number): void; diff --git a/wasm/node_pkg/release/mercury_wasm.d.ts b/wasm/node_pkg/release/mercury_wasm.d.ts index eb3be297..3ef8c7d6 100644 --- a/wasm/node_pkg/release/mercury_wasm.d.ts +++ b/wasm/node_pkg/release/mercury_wasm.d.ts @@ -251,6 +251,13 @@ export function signMessage(statechain_id: string, coin: any): string; */ export function isEnclavePubkeyPartOfCoin(coin: any, enclave_pubkey: string): boolean; /** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +export function latestBackuptxPaysToUserpubkey(backup_transactions: any, coin: any, network: string): any; +/** * @returns {any} */ export function getMockWallet(): any; diff --git a/wasm/node_pkg/release/mercury_wasm.js b/wasm/node_pkg/release/mercury_wasm.js index ff3951d0..d0ce59dd 100644 --- a/wasm/node_pkg/release/mercury_wasm.js +++ b/wasm/node_pkg/release/mercury_wasm.js @@ -793,6 +793,19 @@ module.exports.isEnclavePubkeyPartOfCoin = function(coin, enclave_pubkey) { return ret !== 0; }; +/** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +module.exports.latestBackuptxPaysToUserpubkey = function(backup_transactions, coin, network) { + const ptr0 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.latestBackuptxPaysToUserpubkey(addHeapObject(backup_transactions), addHeapObject(coin), ptr0, len0); + return takeObject(ret); +}; + /** * @returns {any} */ diff --git a/wasm/node_pkg/release/mercury_wasm_bg.wasm b/wasm/node_pkg/release/mercury_wasm_bg.wasm index 8da86cd9..8fc93007 100644 Binary files a/wasm/node_pkg/release/mercury_wasm_bg.wasm and b/wasm/node_pkg/release/mercury_wasm_bg.wasm differ diff --git a/wasm/node_pkg/release/mercury_wasm_bg.wasm.d.ts b/wasm/node_pkg/release/mercury_wasm_bg.wasm.d.ts index fe096d2d..b1a3f9fb 100644 --- a/wasm/node_pkg/release/mercury_wasm_bg.wasm.d.ts +++ b/wasm/node_pkg/release/mercury_wasm_bg.wasm.d.ts @@ -39,6 +39,7 @@ export function getNewKeyInfo(a: number, b: number, c: number, d: number, e: num export function validateAddress(a: number, b: number, c: number, d: number): number; export function signMessage(a: number, b: number, c: number, d: number): void; export function isEnclavePubkeyPartOfCoin(a: number, b: number, c: number): number; +export function latestBackuptxPaysToUserpubkey(a: number, b: number, c: number, d: number): number; export function getMockWallet(): number; export function rustsecp256k1zkp_v0_8_1_default_illegal_callback_fn(a: number, b: number): void; export function rustsecp256k1zkp_v0_8_1_default_error_callback_fn(a: number, b: number): void; diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index f59aa8ff..bb2d17cd 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -439,6 +439,21 @@ pub fn isEnclavePubkeyPartOfCoin(coin: JsValue, enclave_pubkey: String) -> bool mercurylib::utils::is_enclave_pubkey_part_of_coin(&coin, &enclave_pubkey).unwrap() } +#[wasm_bindgen] +pub fn latestBackuptxPaysToUserpubkey(backup_transactions: JsValue, coin: JsValue, network: String) -> JsValue { + + let backup_transactions: Vec = serde_wasm_bindgen::from_value(backup_transactions).unwrap(); + let coin: Coin = serde_wasm_bindgen::from_value(coin).unwrap(); + + let backup_transaction = mercurylib::wallet::cpfp_tx::latest_backup_tx_pays_to_user_pubkey(&backup_transactions, &coin, &network); + + if backup_transaction.is_err() { + return JsValue::NULL; + } else { + serde_wasm_bindgen::to_value(&backup_transaction.unwrap()).unwrap() + } +} + #[wasm_bindgen] pub fn getMockWallet() -> JsValue { let tokens = vec![ diff --git a/wasm/web_pkg/debug/mercury_wasm.d.ts b/wasm/web_pkg/debug/mercury_wasm.d.ts index 28f484ac..6970dd74 100644 --- a/wasm/web_pkg/debug/mercury_wasm.d.ts +++ b/wasm/web_pkg/debug/mercury_wasm.d.ts @@ -251,6 +251,13 @@ export function signMessage(statechain_id: string, coin: any): string; */ export function isEnclavePubkeyPartOfCoin(coin: any, enclave_pubkey: string): boolean; /** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +export function latestBackuptxPaysToUserpubkey(backup_transactions: any, coin: any, network: string): any; +/** * @returns {any} */ export function getMockWallet(): any; @@ -297,6 +304,7 @@ export interface InitOutput { readonly validateAddress: (a: number, b: number, c: number, d: number) => number; readonly signMessage: (a: number, b: number, c: number, d: number) => void; readonly isEnclavePubkeyPartOfCoin: (a: number, b: number, c: number) => number; + readonly latestBackuptxPaysToUserpubkey: (a: number, b: number, c: number, d: number) => number; readonly getMockWallet: () => number; readonly rustsecp256k1zkp_v0_8_1_default_illegal_callback_fn: (a: number, b: number) => void; readonly rustsecp256k1zkp_v0_8_1_default_error_callback_fn: (a: number, b: number) => void; diff --git a/wasm/web_pkg/debug/mercury_wasm.js b/wasm/web_pkg/debug/mercury_wasm.js index baa94da3..9b722f5e 100644 --- a/wasm/web_pkg/debug/mercury_wasm.js +++ b/wasm/web_pkg/debug/mercury_wasm.js @@ -816,6 +816,19 @@ export function isEnclavePubkeyPartOfCoin(coin, enclave_pubkey) { return ret !== 0; } +/** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +export function latestBackuptxPaysToUserpubkey(backup_transactions, coin, network) { + const ptr0 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.latestBackuptxPaysToUserpubkey(addHeapObject(backup_transactions), addHeapObject(coin), ptr0, len0); + return takeObject(ret); +} + /** * @returns {any} */ diff --git a/wasm/web_pkg/debug/mercury_wasm_bg.wasm b/wasm/web_pkg/debug/mercury_wasm_bg.wasm index e8bfa745..4baa688f 100644 Binary files a/wasm/web_pkg/debug/mercury_wasm_bg.wasm and b/wasm/web_pkg/debug/mercury_wasm_bg.wasm differ diff --git a/wasm/web_pkg/debug/mercury_wasm_bg.wasm.d.ts b/wasm/web_pkg/debug/mercury_wasm_bg.wasm.d.ts index fe096d2d..b1a3f9fb 100644 --- a/wasm/web_pkg/debug/mercury_wasm_bg.wasm.d.ts +++ b/wasm/web_pkg/debug/mercury_wasm_bg.wasm.d.ts @@ -39,6 +39,7 @@ export function getNewKeyInfo(a: number, b: number, c: number, d: number, e: num export function validateAddress(a: number, b: number, c: number, d: number): number; export function signMessage(a: number, b: number, c: number, d: number): void; export function isEnclavePubkeyPartOfCoin(a: number, b: number, c: number): number; +export function latestBackuptxPaysToUserpubkey(a: number, b: number, c: number, d: number): number; export function getMockWallet(): number; export function rustsecp256k1zkp_v0_8_1_default_illegal_callback_fn(a: number, b: number): void; export function rustsecp256k1zkp_v0_8_1_default_error_callback_fn(a: number, b: number): void; diff --git a/wasm/web_pkg/release/mercury_wasm.d.ts b/wasm/web_pkg/release/mercury_wasm.d.ts index 28f484ac..6970dd74 100644 --- a/wasm/web_pkg/release/mercury_wasm.d.ts +++ b/wasm/web_pkg/release/mercury_wasm.d.ts @@ -251,6 +251,13 @@ export function signMessage(statechain_id: string, coin: any): string; */ export function isEnclavePubkeyPartOfCoin(coin: any, enclave_pubkey: string): boolean; /** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +export function latestBackuptxPaysToUserpubkey(backup_transactions: any, coin: any, network: string): any; +/** * @returns {any} */ export function getMockWallet(): any; @@ -297,6 +304,7 @@ export interface InitOutput { readonly validateAddress: (a: number, b: number, c: number, d: number) => number; readonly signMessage: (a: number, b: number, c: number, d: number) => void; readonly isEnclavePubkeyPartOfCoin: (a: number, b: number, c: number) => number; + readonly latestBackuptxPaysToUserpubkey: (a: number, b: number, c: number, d: number) => number; readonly getMockWallet: () => number; readonly rustsecp256k1zkp_v0_8_1_default_illegal_callback_fn: (a: number, b: number) => void; readonly rustsecp256k1zkp_v0_8_1_default_error_callback_fn: (a: number, b: number) => void; diff --git a/wasm/web_pkg/release/mercury_wasm.js b/wasm/web_pkg/release/mercury_wasm.js index 5383a47a..bd4b3e60 100644 --- a/wasm/web_pkg/release/mercury_wasm.js +++ b/wasm/web_pkg/release/mercury_wasm.js @@ -790,6 +790,19 @@ export function isEnclavePubkeyPartOfCoin(coin, enclave_pubkey) { return ret !== 0; } +/** +* @param {any} backup_transactions +* @param {any} coin +* @param {string} network +* @returns {any} +*/ +export function latestBackuptxPaysToUserpubkey(backup_transactions, coin, network) { + const ptr0 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.latestBackuptxPaysToUserpubkey(addHeapObject(backup_transactions), addHeapObject(coin), ptr0, len0); + return takeObject(ret); +} + /** * @returns {any} */ diff --git a/wasm/web_pkg/release/mercury_wasm_bg.wasm b/wasm/web_pkg/release/mercury_wasm_bg.wasm index fa5139b2..a35151bc 100644 Binary files a/wasm/web_pkg/release/mercury_wasm_bg.wasm and b/wasm/web_pkg/release/mercury_wasm_bg.wasm differ diff --git a/wasm/web_pkg/release/mercury_wasm_bg.wasm.d.ts b/wasm/web_pkg/release/mercury_wasm_bg.wasm.d.ts index fe096d2d..b1a3f9fb 100644 --- a/wasm/web_pkg/release/mercury_wasm_bg.wasm.d.ts +++ b/wasm/web_pkg/release/mercury_wasm_bg.wasm.d.ts @@ -39,6 +39,7 @@ export function getNewKeyInfo(a: number, b: number, c: number, d: number, e: num export function validateAddress(a: number, b: number, c: number, d: number): number; export function signMessage(a: number, b: number, c: number, d: number): void; export function isEnclavePubkeyPartOfCoin(a: number, b: number, c: number): number; +export function latestBackuptxPaysToUserpubkey(a: number, b: number, c: number, d: number): number; export function getMockWallet(): number; export function rustsecp256k1zkp_v0_8_1_default_illegal_callback_fn(a: number, b: number): void; export function rustsecp256k1zkp_v0_8_1_default_error_callback_fn(a: number, b: number): void;