From 54e0e54b4bd71201343f0f37cdf78168be15f832 Mon Sep 17 00:00:00 2001 From: jlest01 <174762002+jlest01@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:59:25 -0300 Subject: [PATCH] Update `submitpackage` RPC --- client/src/client.rs | 16 +++++++ integration_test/src/main.rs | 90 ++++++++++++++++++++++++++++++++++++ json/src/lib.rs | 50 ++++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/client/src/client.rs b/client/src/client.rs index 2f809a79..71a0c40b 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -868,6 +868,22 @@ pub trait RpcApi: Sized { self.call("testmempoolaccept", &[hexes.into()]) } + /// Submit a package of raw transactions to the node. The package will be + /// validated according to consensus and mempool policy rules. If all + /// transactions pass, they will be accepted to mempool. + fn submit_package( + &self, + rawtxs: &[R], + maxfeerate: Option, + maxburnamount: Option, + ) -> Result { + let hexes: Vec = + rawtxs.to_vec().into_iter().map(|r| r.raw_hex().into()).collect(); + let mut args = [hexes.into(), opt_into_json(maxfeerate)?, opt_into_json(maxburnamount)?]; + let defaults = [null(), null()]; + self.call("submitpackage", handle_defaults(&mut args, &defaults)) + } + fn stop(&self) -> Result { self.call("stop", &[]) } diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs index c3236bb9..a7d49b97 100644 --- a/integration_test/src/main.rs +++ b/integration_test/src/main.rs @@ -182,6 +182,7 @@ fn main() { test_decode_raw_transaction(&cl); test_fund_raw_transaction(&cl); test_test_mempool_accept(&cl); + test_submit_package(&cl); test_wallet_create_funded_psbt(&cl); test_wallet_process_psbt(&cl); test_join_psbt(&cl); @@ -770,6 +771,95 @@ fn test_test_mempool_accept(cl: &Client) { assert!(res[0].allowed, "not allowed: {:?}", res[0].reject_reason); } +fn test_submit_package(cl: &Client) { + let sk = PrivateKey { + network: Network::Regtest.into(), + inner: secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()), + compressed: true, + }; + let pk = CompressedPublicKey::from_private_key(&SECP, &sk).unwrap(); + let addr = Address::p2wpkh(&pk, Network::Regtest); + + let options = json::ListUnspentQueryOptions { + minimum_amount: Some(btc(2)), + ..Default::default() + }; + let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = unspent.into_iter().nth(0).unwrap(); + + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: unspent.txid, + vout: unspent.vout, + }, + sequence: Sequence::MAX, + script_sig: ScriptBuf::new(), + witness: Witness::new(), + }], + output: vec![TxOut { + value: (unspent.amount - *FEE), + script_pubkey: addr.script_pubkey(), + }], + }; + + let input = json::SignRawTransactionInput { + txid: unspent.txid, + vout: unspent.vout, + script_pub_key: unspent.script_pub_key, + redeem_script: None, + amount: Some(unspent.amount), + }; + let res1 = cl.sign_raw_transaction_with_wallet(&tx, Some(&[input]), None).unwrap(); + assert!(res1.complete); + let tx1 = res1.transaction().unwrap(); + let txid1 = tx1.compute_txid(); + + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: txid1, + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: (unspent.amount - *FEE - *FEE), + script_pubkey: RANDOM_ADDRESS.script_pubkey(), + }], + }; + + let input = json::SignRawTransactionInput { + txid: txid1, + vout: 0, + script_pub_key: tx1.output[0].script_pubkey.clone(), + redeem_script: None, + amount: Some(tx1.output[0].value), + }; + + let res2 = cl + .sign_raw_transaction_with_key(&tx, &[sk], Some(&[input]), Some(sighash::EcdsaSighashType::All.into())) + .unwrap(); + assert!(res2.complete); + + let tx2 = res2.transaction().unwrap(); + + let signed_transactions = vec![tx1, tx2]; + + let signed_refs: Vec<&Transaction> = signed_transactions.iter().map(|s| s).collect(); + let signed_refs = signed_refs.as_slice(); + + let res = cl.submit_package(signed_refs, None, None).unwrap(); + + assert!(res.package_msg == "success"); +} + fn test_wallet_create_funded_psbt(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap(); let options = json::ListUnspentQueryOptions { diff --git a/json/src/lib.rs b/json/src/lib.rs index 25f3508e..a1388b8b 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -852,6 +852,56 @@ pub struct TestMempoolAcceptResultFees { // unlike GetMempoolEntryResultFees, this only has the `base` fee } +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct Fees { + /// Transaction fee. + #[serde(with = "bitcoin::amount::serde::as_btc")] + pub base: Amount, + + /// If the transaction was not already in the mempool, the effective feerate + /// in BTC per KvB. For example, the package feerate and/or feerate with + /// modified fees from prioritisetransaction. + #[serde(default, rename = "effective-feerate", with = "bitcoin::amount::serde::as_btc::opt")] + pub effective_feerate: Option, + + /// If effective-feerate is provided, the wtxids of the transactions whose + /// fees and vsizes are included in effective-feerate. + #[serde(rename = "effective-includes")] + pub effective_includes: Option>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct TxResult { + pub txid: bitcoin::Txid, + + /// The wtxid of a different transaction with the same txid but different + /// witness found in the mempool. This means the submitted transaction was + /// ignored. + #[serde(rename = "other-wtxid")] + pub other_wtxid: Option, + + /// Virtual transaction size as defined in BIP 141. + pub vsize: Option, + + pub fees: Option, + + pub error: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct SubmitPackageResult { + /// The transaction package result message. "success" indicates all transactions were accepted into or are already in the mempool + pub package_msg: String, + + /// Transaction results keyed by wtxid. + #[serde(rename = "tx-results")] + pub tx_results: HashMap, + + /// List of txids of replaced transactions. + #[serde(rename = "replaced-transactions")] + pub replaced_transactions: Vec, +} + #[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum Bip9SoftforkStatus {