Combining multiple Bitcoin transaction

Hello,

I’m trying to implement the logic, where bitcoin are sent from multiple wallet/address to one or more receivers.

Is it possible to do such thing?

I tried it implementing. Here is my code so far

fn transfer(account0: Account,
        account1: Account,
        address0: Address,
        address1: Address,
        utxos0: Vec<Utxo>,
        utxos1: Vec<Utxo>,
        amount0: u64,
        amount1: u64,
        fee: u64,
        paid_by_sender: bool,
        receiver: Address,
){
                const DUST_THRESHOLD: u64 = 1_000;
                let mut input = Vec::with_capacity(utxos0.len() + utxos1.len());
                let mut index_of_utxos_of_addr0 = vec![];
                let mut index_of_utxos_of_addr1 = vec![];
                let (mut total_spent0, mut total_spent1) = (0, 0);

                utxos0.iter().for_each(|utxo| {
                    let txin = TxIn {
                        sequence: Sequence::max_value(),
                        script_sig: ScriptBuf::new(),
                        witness: Witness::new(),
                        previous_output: OutPoint {
                            txid: Txid::from_raw_hash(
                                Hash::from_slice(&utxo.outpoint.txid).expect("should return hash"),
                            ),
                            vout: utxo.outpoint.vout,
                        },
                    };
                    total_spent0 += utxo.value;
                    let current_len = input.len();
                    input.insert(current_len, txin);
                    index_of_utxos_of_addr0.push(current_len);
                });
                utxos1.iter().for_each(|utxo| {
                    let txin = TxIn {
                        sequence: Sequence::max_value(),
                        script_sig: ScriptBuf::new(),
                        witness: Witness::new(),
                        previous_output: OutPoint {
                            txid: Txid::from_raw_hash(
                                Hash::from_slice(&utxo.outpoint.txid).expect("should return hash"),
                            ),
                            vout: utxo.outpoint.vout,
                        },
                    };
                    total_spent1 += utxo.value;
                    let current_len = input.len();
                    input.insert(current_len, txin);
                    index_of_utxos_of_addr1.push(current_len);
                });

                let mut output = vec![TxOut {
                    script_pubkey: receiver.script_pubkey(),
                    value: if *paid_by_sender {
                        amount0 + amount1
                    } else {
                        amount0 + amount1 - fee
                    },
                }];

                // block responsible for calculating and adding remaining account
                {
                    let (fee0, fee1) = {
                        let is_even = fee % 2 == 0;
                        if is_even {
                            let fee_in_half = fee / 2;
                            (fee_in_half, fee_in_half)
                        } else {
                            let fee_in_half = (fee - 1) / 2;
                            (fee_in_half, fee_in_half + 1)
                        }
                    };
                    let (amount0, amount1) = if *paid_by_sender {
                        (amount0 + fee0, amount1 + fee1)
                    } else {
                        (*amount0, *amount1)
                    };
                    let remaining0 = total_spent0 - amount0;
                    if remaining0 > DUST_THRESHOLD {
                        output.push(TxOut {
                            script_pubkey: address0.script_pubkey(),
                            value: remaining0,
                        });
                    }
                    let remaining1 = total_spent1 - amount1;
                    if remaining1 > DUST_THRESHOLD {
                        output.push(TxOut {
                            script_pubkey: address1.script_pubkey(),
                            value: remaining1,
                        })
                    }
                }

                let mut txn = Transaction {
                    input,
                    output,
                    lock_time: LockTime::ZERO,
                    version: 2,
                };

                // signing the transaction

                let (path0, pubkey0, path1, pubkey1) = read_config(|config| {
                    let ecdsa_key = config.ecdsa_public_key();
                    let path0 = account_to_derivation_path(account0);
                    let path1 = account_to_derivation_path(account1);
                    let pubkey0 = derive_public_key(&ecdsa_key, &path0).public_key;
                    let pubkey1 = derive_public_key(&ecdsa_key, &path1).public_key;
                    (
                        DerivationPath::new(path0),
                        pubkey0,
                        DerivationPath::new(path1),
                        pubkey1,
                    )
                });
                let txn_cache = SighashCache::new(txn.clone());
                for (i, input) in txn.input.iter_mut().enumerate() {
                    if index_of_utxos_of_addr0.contains(&i) {
                        let sighash = txn_cache
                            .legacy_signature_hash(
                                i,
                                &address0.script_pubkey(),
                                EcdsaSighashType::All.to_u32(),
                            )
                            .unwrap();
                        let signature = ecdsa_sign(
                            sighash.as_byte_array().to_vec(),
                            path0.clone().into_inner(),
                        )
                        .await
                        .signature;
                        let mut signature = sec1_to_der(signature);
                        signature.push(EcdsaSighashType::All.to_u32() as u8);
                        let signature = PushBytesBuf::try_from(signature).unwrap();
                        let pubkey = PushBytesBuf::try_from(pubkey0.clone()).unwrap();
                        input.script_sig = Builder::new()
                            .push_slice(signature)
                            .push_slice(pubkey)
                            .into_script();
                        input.witness.clear();
                    } else {
                        let sighash = txn_cache
                            .legacy_signature_hash(
                                i,
                                &address1.script_pubkey(),
                                EcdsaSighashType::All.to_u32(),
                            )
                            .unwrap();
                        let signature = ecdsa_sign(
                            sighash.as_byte_array().to_vec(),
                            path1.clone().into_inner(),
                        )
                        .await
                        .signature;
                        let mut signature = sec1_to_der(signature);
                        signature.push(EcdsaSighashType::All.to_u32() as u8);
                        let signature = PushBytesBuf::try_from(signature).unwrap();
                        let pubkey = PushBytesBuf::try_from(pubkey1.clone()).unwrap();
                        input.script_sig = Builder::new()
                            .push_slice(signature)
                            .push_slice(pubkey)
                            .into_script();
                        input.witness.clear();
                    }
                }
                let txid = txn.txid().to_string();
                let txn_bytes = bitcoin::consensus::serialize(&txn);
                ic_cdk::println!("{}", hex::encode(&txn_bytes));
                bitcoin_send_transaction(SendTransactionRequest {
                    network: read_config(|config| config.bitcoin_network()),
                    transaction: txn_bytes,
                })
                .await
                .expect("failed to submit transaction");
}

running the command testmempoolaccept returns:

docker compose exec bitcoind bitcoin-cli testmempoolaccept '["0200000002110a7b3d523d844b6df897abd6ceeeb4a58e3a23a6c4ab21ffb536b12206d32c000000006b483045022100
84cd95cb3f476a41d9a3105d5de09c58d1f5e7ae0ddeca30bcee2b4eeb901030022047a4b885496f58145cc3e6d7f7913628df3f242416b779a8a0873a500c3c8d4301210245920894fbc6be26ae3846
bba49c500f173d9808003885b824d897562a1dcb04ffffffff562fdea94d471d0dd24a50e8115d42124f217032b655950e7e41f20ef4cb95db000000006a4730440220048ad77430fbac066b7acb2c12
8eccba3814495e100cdf4584f62c6cf4cfd3f3022025a0d3898123ff646cf0afbac8ff17ce00ddad2d067d07cc410ddc69438b43ab012102f9a399aff43b35f2c6a54b94c5b4713d4147afc7321e3706
68869541dd9d37f4ffffffff0380969800000000001976a9144bb8e5433188d3469cdb8f58b20d8b9aac996e4488ac26acb694000000001976a9144d9f4cfd3c937800e6ee1d1175f9c79ec498ac7788
ac26acb694000000001976a914bd22b275bdb35f16b8e176f469400931cda803ec88ac00000000"]'
[
  {
    "txid": "5cf49ef507c06641fb44116845a3f378a06eedcc0fd3e0d0843000bcad5875ea",
    "wtxid": "5cf49ef507c06641fb44116845a3f378a06eedcc0fd3e0d0843000bcad5875ea",
    "allowed": false,
    "reject-reason": "txn-already-in-mempool"
  }
]

You’re creating a Bitcoin transaction with multiple inputs and (potentially) multiple outputs, right?
This is certainly possible.

Just looking at the error message, the reason for the rejection is txn-already-in-mempool, which means that some transaction that spends some of the same outputs as this transaction is already in the mempool. So, maybe it is sufficient to reset your mempool to get the transaction accepted.

2 Likes

thank you @THLO ,

i’ll check and submit the txn again.

also giving a quick look, do you see any issue in my code?

Is signing part of the code, correct?

Unfortunately, I don’t have the time to review the code right now. How about we do it the lazy way and you first try it out? :slightly_smiling_face:

1 Like

hello @THLO ,

thanks. I tried and the code works, I also see the balances being reflected.

I was just wondering if my singing logic for the transaction was correct or not !
OR might be a better way to sign in, when utxos are combined from different addresses.

If bitcoind accepts the transaction, then you must have done it right!

When skimming through your code, I noticed that signatures are requested sequentially (i.e., you always call await). You may want to consider sending requests in parallel to minimize latency.
You may also want to switch from P2PKH to P2WPKH to save a bit on fees. :slightly_smiling_face:

1 Like

@THLO

I noticed a strange behavior in my localhost.

I’m performing runestone and bitcoin transaction.
When I submit the transaction, it works. I can get the transaction detail using getmempoolentry. but balances aren’t reflected. when I run testmempoolaccept for the first transaction’s bytes I get txn-already-in-mempool.
To get balances reflected, I need to submit the transaction again. when I check for getmempoolentry for second txid, it doesn’t works.

Sorry,
I was mining the transaction in wrong order.

BIG SKILL ISSUES :smiling_face_with_tear:

My problem is fixed.

Thank you so much @THLO for previous replies.

Hello @THLO , I didn’t understood this part. Can you explain this?
Do you mean spawning a thread using ic_cdk::spawn?

also some code examples will help more!

I didn’t understood this part. Can you explain this?
Do you mean spawning a thread using ic_cdk::spawn?

Check out the code here as an example!
Multiple asynchronous calls are triggered in parallel and then all results are collected before proceeding with the execution.

1 Like