finish improvised psbt assembly coordinator side.

This commit is contained in:
f321x
2024-08-01 19:01:29 +02:00
parent ce24a95c4d
commit f45703aa63
15 changed files with 266 additions and 102 deletions

View File

@ -288,6 +288,15 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitcoin"
version = "0.30.2"
@ -465,6 +474,7 @@ dependencies = [
"anyhow",
"axum",
"bdk",
"bincode",
"bitcoin 0.32.2",
"dotenvy",
"env_logger",

View File

@ -27,6 +27,7 @@ env_logger = "0.11"
sha2 = "0.10"
validator = { version = "0.18", features = ["derive"] }
musig2 = "0.0.11"
bincode = "1.3.3"
[profile.release]
lto = true

View File

@ -64,6 +64,7 @@ pub struct PublicOffers {
#[derive(Serialize, Debug, Deserialize)]
pub struct OfferTakenResponse {
pub escrow_psbt_hex: String,
pub escrow_output_descriptor: String,
pub escrow_tx_fee_address: String,
pub escrow_amount_maker_sat: u64,

View File

@ -133,7 +133,7 @@ pub async fn handle_taker_bond(
}
debug!("\nTaker bond validation successful");
let escrow_output_data = match wallet.get_escrow_psbt(database, &payload).await {
let escrow_output_data = match wallet.create_escrow_psbt(database, &payload).await {
Ok(escrow_output_data) => escrow_output_data,
Err(e) => {
return Err(BondError::CoordinatorError(e.to_string()));
@ -148,6 +148,7 @@ pub async fn handle_taker_bond(
}
Ok(OfferTakenResponse {
escrow_psbt_hex: escrow_output_data.escrow_psbt_hex,
escrow_output_descriptor: escrow_output_data.escrow_output_descriptor,
escrow_tx_fee_address: escrow_output_data.escrow_tx_fee_address,
escrow_amount_maker_sat: escrow_output_data.escrow_amount_maker_sat,

View File

@ -5,8 +5,10 @@ use anyhow::Context;
use futures_util::StreamExt;
use super::*;
use bdk::bitcoin::address::Address;
use sqlx::{sqlite::SqlitePoolOptions, Pool, Row, Sqlite};
use std::env;
use std::str::FromStr;
#[derive(Clone, Debug)]
pub struct CoordinatorDB {
@ -692,29 +694,29 @@ impl CoordinatorDB {
Ok(winner_robohash)
}
pub async fn fetch_escrow_tx_payout_data(
&self,
offer_id: &str,
) -> Result<EscrowPsbtConstructionData> {
let row = sqlx::query("SELECT taproot_xonly_pubkey_hex_maker, taproot_xonly_pubkey_hex_taker, musig_pubkey_compressed_hex_maker, musig_pubkey_compressed_hex_taker FROM taken_offers WHERE offer_id = ?")
.bind(offer_id)
.fetch_one(&*self.db_pool)
.await?;
// pub async fn fetch_escrow_tx_payout_data(
// &self,
// offer_id: &str,
// ) -> Result<EscrowPsbtConstructionData> {
// let row = sqlx::query("SELECT taproot_xonly_pubkey_hex_maker, taproot_xonly_pubkey_hex_taker, musig_pubkey_compressed_hex_maker, musig_pubkey_compressed_hex_taker FROM taken_offers WHERE offer_id = ?")
// .bind(offer_id)
// .fetch_one(&*self.db_pool)
// .await?;
let taproot_xonly_pubkey_hex_maker: String = row.get("taproot_xonly_pubkey_hex_maker");
let taproot_xonly_pubkey_hex_taker: String = row.get("taproot_xonly_pubkey_hex_taker");
let musig_pubkey_compressed_hex_maker: String =
row.get("musig_pubkey_compressed_hex_maker");
let musig_pubkey_compressed_hex_taker: String =
row.get("musig_pubkey_compressed_hex_taker");
// let taproot_xonly_pubkey_hex_maker: String = row.get("taproot_xonly_pubkey_hex_maker");
// let taproot_xonly_pubkey_hex_taker: String = row.get("taproot_xonly_pubkey_hex_taker");
// let musig_pubkey_compressed_hex_maker: String =
// row.get("musig_pubkey_compressed_hex_maker");
// let musig_pubkey_compressed_hex_taker: String =
// row.get("musig_pubkey_compressed_hex_taker");
Ok(EscrowPsbtConstructionData {
taproot_xonly_pubkey_hex_maker,
taproot_xonly_pubkey_hex_taker,
musig_pubkey_compressed_hex_maker,
musig_pubkey_compressed_hex_taker,
})
}
// Ok(EscrowPsbtConstructionData {
// taproot_xonly_pubkey_hex_maker,
// taproot_xonly_pubkey_hex_taker,
// musig_pubkey_compressed_hex_maker,
// musig_pubkey_compressed_hex_taker,
// })
// }
pub async fn get_escrow_tx_amounts(
&self,
@ -743,4 +745,26 @@ impl CoordinatorDB {
escrow_fee_per_participant,
))
}
pub async fn fetch_maker_escrow_psbt_data(
&self,
trade_id: &str,
) -> Result<EscrowPsbtConstructionData> {
let row = sqlx::query(
"SELECT escrow_inputs_hex_maker_csv, change_address_maker, taproot_pubkey_hex_maker, musig_pubkey_hex FROM active_maker_offers WHERE offer_id = ?",
)
.bind(trade_id)
.fetch_one(&*self.db_pool)
.await?;
let deserialized_inputs = csv_hex_to_bdk_input(row.get("escrow_inputs_hex_maker_csv"))?;
let change_address: String = row.get("change_address_maker");
Ok(EscrowPsbtConstructionData {
escrow_input_utxos: deserialized_inputs,
change_address: Address::from_str(&change_address)?.assume_checked(),
taproot_xonly_pubkey_hex: row.get("taproot_pubkey_hex_maker"),
musig_pubkey_compressed_hex: row.get("musig_pubkey_hex"),
})
}
}

View File

@ -21,7 +21,7 @@ use std::{
};
use tokio::sync::Mutex;
use validator::{Validate, ValidationError};
use wallet::{escrow_psbt::*, *};
use wallet::{escrow_psbt::*, wallet_utils::*, *};
pub struct Coordinator {
pub coordinator_db: Arc<CoordinatorDB>,

View File

@ -1,5 +1,6 @@
use super::*;
use bdk::{
bitcoin::psbt::Input,
descriptor::Descriptor,
miniscript::{descriptor::TapTree, policy::Concrete, Tap},
};
@ -7,11 +8,28 @@ use musig2::{secp256k1::PublicKey as MuSig2PubKey, KeyAggContext};
#[derive(Debug)]
pub struct EscrowPsbtConstructionData {
pub taproot_xonly_pubkey_hex_maker: String,
pub taproot_xonly_pubkey_hex_taker: String,
pub taproot_xonly_pubkey_hex: String,
pub escrow_input_utxos: Vec<PsbtInput>,
pub change_address: Address,
// pub taproot_xonly_pubkey_hex_taker: String,
// pub taproot_pubkey_hex_coordinator: String,
pub musig_pubkey_compressed_hex_maker: String,
pub musig_pubkey_compressed_hex_taker: String,
pub musig_pubkey_compressed_hex: String,
// pub musig_pubkey_compressed_hex_taker: String,
}
impl EscrowPsbtConstructionData {
pub fn input_sum(&self) -> Result<u64> {
let mut input_sum = 0;
for input in &self.escrow_input_utxos {
if let Some(txout) = input.psbt_input.witness_utxo.as_ref() {
input_sum += txout.value;
}
}
if input_sum == 0 {
return Err(anyhow!("Input sum of escrow creation psbt input is 0"));
}
Ok(input_sum)
}
}
fn aggregate_musig_pubkeys(maker_musig_pubkey: &str, taker_musig_pubkey: &str) -> Result<String> {
@ -31,12 +49,12 @@ fn aggregate_musig_pubkeys(maker_musig_pubkey: &str, taker_musig_pubkey: &str) -
}
pub fn build_escrow_transaction_output_descriptor(
escrow_data: &EscrowPsbtConstructionData,
maker_escrow_data: &EscrowPsbtConstructionData,
taker_escrow_data: &EscrowPsbtConstructionData,
coordinator_pk: &XOnlyPublicKey,
) -> Result<String> {
let taproot_pubkey_hex_maker = escrow_data.taproot_xonly_pubkey_hex_maker.clone();
let maker_pk = taproot_pubkey_hex_maker;
let taker_pk = escrow_data.taproot_xonly_pubkey_hex_taker.clone();
let maker_pk = maker_escrow_data.taproot_xonly_pubkey_hex.clone();
let taker_pk = taker_escrow_data.taproot_xonly_pubkey_hex.clone();
let coordinator_pk = hex::encode(coordinator_pk.serialize());
// let script_a = format!("and(and(after({}),{}),{})", "144", maker_pk, coordinator_pk);
@ -84,8 +102,8 @@ pub fn build_escrow_transaction_output_descriptor(
// An internal key, that defines the way to spend the transaction directly, using Key Path Spending
let internal_agg_musig_key = aggregate_musig_pubkeys(
&escrow_data.musig_pubkey_compressed_hex_maker,
&escrow_data.musig_pubkey_compressed_hex_taker,
&maker_escrow_data.musig_pubkey_compressed_hex,
&taker_escrow_data.musig_pubkey_compressed_hex,
)?;
// Create the descriptor
@ -97,11 +115,11 @@ pub fn build_escrow_transaction_output_descriptor(
Ok(descriptor.to_string())
}
pub fn assemble_escrow_psbts(
coordinator: &Coordinator,
escrow_data: &EscrowPsbtConstructionData,
coordinator_pk: &XOnlyPublicKey,
) -> Result<()> {
panic!("Implement wallet.build_escrow_psbt()");
Ok(())
}
// pub fn assemble_escrow_psbts(
// coordinator: &Coordinator,
// escrow_data: &EscrowPsbtConstructionData,
// coordinator_pk: &XOnlyPublicKey,
// ) -> Result<()> {
// panic!("Implement wallet.build_escrow_psbt()");
// Ok(())
// }

View File

@ -1,5 +1,5 @@
pub mod escrow_psbt;
mod utils;
pub mod wallet_utils;
// pub mod verify_tx;
#[cfg(test)]
mod wallet_tests;
@ -14,21 +14,22 @@ use bdk::{
bip32::ExtendedPrivKey,
consensus::encode::deserialize,
key::{secp256k1, XOnlyPublicKey},
taproot::TapLeaf,
Address,
Network::Regtest,
Transaction,
},
bitcoincore_rpc::{Client, RawTx, RpcApi},
blockchain::{rpc::Auth, Blockchain, ConfigurableBlockchain, RpcBlockchain, RpcConfig},
database::MemoryDatabase,
sled::{self, Tree},
template::Bip86,
wallet::verify::*,
KeychainKind, SyncOptions, Wallet,
};
use coordinator::mempool_monitoring::MempoolHandler;
use core::panic;
use std::{collections::HashMap, str::FromStr};
use std::{fmt, ops::Deref};
use utils::*;
// use verify_tx::*;
#[derive(Clone)]
@ -42,6 +43,7 @@ pub struct CoordinatorWallet<D: bdk::database::BatchDatabase> {
#[derive(Debug)]
pub struct EscrowPsbt {
pub escrow_psbt_hex: String,
pub escrow_output_descriptor: String,
pub escrow_tx_fee_address: String,
pub coordinator_xonly_escrow_pk: String,
@ -237,25 +239,92 @@ impl<D: bdk::database::BatchDatabase> CoordinatorWallet<D> {
Ok(())
}
pub async fn get_escrow_psbt(
pub async fn create_escrow_psbt(
&self,
db: &Arc<CoordinatorDB>,
taker_psbt_request: &OfferPsbtRequest,
) -> Result<EscrowPsbt> {
let trade_id = &taker_psbt_request.offer.offer_id_hex;
panic!("adjust");
let escrow_pubkeys = db.fetch_escrow_tx_payout_data(trade_id).await?;
let trade_id = &taker_psbt_request.offer.offer_id_hex.clone();
let maker_psbt_input_data = db.fetch_maker_escrow_psbt_data(trade_id).await?;
let taker_psbt_input_data = EscrowPsbtConstructionData {
taproot_xonly_pubkey_hex: taker_psbt_request.trade_data.taproot_pubkey_hex.clone(),
escrow_input_utxos: csv_hex_to_bdk_input(
&taker_psbt_request.trade_data.bdk_psbt_inputs_hex_csv,
)?,
change_address: Address::from_str(
&taker_psbt_request.trade_data.client_change_address,
)?
.assume_checked(),
musig_pubkey_compressed_hex: taker_psbt_request.trade_data.musig_pubkey_hex.clone(),
};
let coordinator_escrow_pk = self.get_coordinator_taproot_pk().await?;
let escrow_output_descriptor =
build_escrow_transaction_output_descriptor(&escrow_pubkeys, &coordinator_escrow_pk)?;
let escrow_tx_fee_address = self.get_new_address().await?;
let escrow_output_descriptor = build_escrow_transaction_output_descriptor(
&maker_psbt_input_data,
&taker_psbt_input_data,
&coordinator_escrow_pk,
)?;
let escrow_coordinator_fee_address =
Address::from_str(&self.get_new_address().await?)?.assume_checked();
let (escrow_amount_maker_sat, escrow_amount_taker_sat, escrow_fee_sat_per_participant) = db
.get_escrow_tx_amounts(trade_id, self.coordinator_feerate)
.await?;
let (escrow_psbt, details) = {
// maybe we can generate a address/taproot pk directly from the descriptor without a new wallet?
let temp_wallet = Wallet::new(
&escrow_output_descriptor,
None,
bitcoin::Network::Regtest,
MemoryDatabase::new(),
)?;
let escrow_address = temp_wallet
.get_address(bdk::wallet::AddressIndex::New)?
.address;
// using absolute fee for now, in production we should come up with a way to determine the tx weight
// upfront and substract the fee from the change outputs
let tx_fee_abs = 10000;
let change_amount_maker = maker_psbt_input_data.input_sum()?
- (escrow_amount_maker_sat + escrow_fee_sat_per_participant + tx_fee_abs / 2);
let change_amount_taker = taker_psbt_input_data.input_sum()?
- (escrow_amount_taker_sat + escrow_fee_sat_per_participant + tx_fee_abs / 2);
let amount_escrow = escrow_amount_maker_sat + escrow_amount_taker_sat;
let mut builder = temp_wallet.build_tx();
builder
.manually_selected_only()
.add_recipient(escrow_address.script_pubkey(), amount_escrow)
.add_recipient(
escrow_coordinator_fee_address.script_pubkey(),
escrow_fee_sat_per_participant * 2,
)
.add_recipient(
maker_psbt_input_data.change_address.script_pubkey(),
change_amount_maker,
)
.add_recipient(
taker_psbt_input_data.change_address.script_pubkey(),
change_amount_taker,
)
.fee_absolute(tx_fee_abs);
for input in maker_psbt_input_data.escrow_input_utxos.iter() {
// satisfaction weight 66 bytes for schnorr sig + opcode + sighash for keyspend. This is a hack?
builder.add_foreign_utxo(input.utxo, input.psbt_input.clone(), 264);
}
for input in taker_psbt_input_data.escrow_input_utxos.iter() {
builder.add_foreign_utxo(input.utxo, input.psbt_input.clone(), 264);
}
builder.finish()?
};
Ok(EscrowPsbt {
escrow_psbt_hex: escrow_psbt.to_string(),
escrow_output_descriptor,
escrow_tx_fee_address,
escrow_tx_fee_address: escrow_coordinator_fee_address.to_string(),
coordinator_xonly_escrow_pk: coordinator_escrow_pk.to_string(),
escrow_amount_maker_sat,
escrow_amount_taker_sat,

View File

@ -1,9 +1,16 @@
use super::*;
use bdk::{
bitcoin::{Address, Network},
bitcoin::{psbt::Input, Address, Network},
blockchain::GetTx,
database::Database,
};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct PsbtInput {
pub psbt_input: Input,
pub utxo: bdk::bitcoin::OutPoint,
}
pub trait BondTx {
fn input_sum<D: Database, B: GetTx>(&self, blockchain: &B, db: &D) -> Result<u64>;
@ -56,3 +63,16 @@ impl BondTx for Transaction {
self.output.iter().map(|output| output.value).sum()
}
}
pub fn csv_hex_to_bdk_input(inputs_csv_hex: &str) -> Result<Vec<PsbtInput>> {
let mut inputs: Vec<PsbtInput> = Vec::new();
for input_hex in inputs_csv_hex.split(',') {
let binary = hex::decode(input_hex)?;
let input: PsbtInput = bincode::deserialize(&binary)?;
inputs.push(input);
}
if inputs.is_empty() {
return Err(anyhow!("No inputs found in csv input value"));
}
Ok(inputs)
}

View File

@ -48,6 +48,7 @@ pub struct OfferTakenRequest {
#[derive(Debug, Deserialize)]
pub struct OfferTakenResponse {
pub escrow_psbt_hex: String,
pub escrow_output_descriptor: String,
pub escrow_tx_fee_address: String,
pub escrow_amount_maker_sat: u64,

View File

@ -66,51 +66,51 @@ impl BondRequirementResponse {
}
impl BondSubmissionRequest {
pub fn prepare_bond_request(
bond: &PartiallySignedTransaction,
payout_address: &AddressInfo,
musig_data: &mut MuSigData,
trader_config: &TraderSettings,
taproot_pubkey: &XOnlyPublicKey,
) -> Result<BondSubmissionRequest> {
let signed_bond_hex = serialize_hex(&bond.to_owned().extract_tx());
let musig_pub_nonce_hex = hex::encode(musig_data.nonce.get_pub_for_sharing()?.serialize());
let musig_pubkey_hex = hex::encode(musig_data.public_key.to_string());
let taproot_pubkey_hex = hex::encode(taproot_pubkey.serialize());
// pub fn prepare_bond_request(
// bond: &partiallysignedtransaction,
// payout_address: &addressinfo,
// musig_data: &mut musigdata,
// trader_config: &tradersettings,
// taproot_pubkey: &xonlypublickey,
// ) -> result<bondsubmissionrequest> {
// let signed_bond_hex = serialize_hex(&bond.to_owned().extract_tx());
// let musig_pub_nonce_hex = hex::encode(musig_data.nonce.get_pub_for_sharing()?.serialize());
// let musig_pubkey_hex = hex::encode(musig_data.public_key.to_string());
// let taproot_pubkey_hex = hex::encode(taproot_pubkey.serialize());
let request = BondSubmissionRequest {
robohash_hex: trader_config.robosats_robohash_hex.clone(),
signed_bond_hex,
payout_address: payout_address.address.to_string(),
musig_pub_nonce_hex,
musig_pubkey_hex,
taproot_pubkey_hex,
};
Ok(request)
}
// let request = bondsubmissionrequest {
// robohash_hex: trader_config.robosats_robohash_hex.clone(),
// signed_bond_hex,
// payout_address: payout_address.address.to_string(),
// musig_pub_nonce_hex,
// musig_pubkey_hex,
// taproot_pubkey_hex,
// };
// ok(request)
// }
pub fn send_maker(
robohash_hex: &str,
bond: &PartiallySignedTransaction,
musig_data: &mut MuSigData,
payout_address: &AddressInfo,
&self, // robohash_hex: &str,
// bond: &PartiallySignedTransaction,
// musig_data: &mut MuSigData,
// payout_address: &AddressInfo,
trader_setup: &TraderSettings,
taproot_pubkey: &XOnlyPublicKey,
// taproot_pubkey: &XOnlyPublicKey,
) -> Result<OrderActivatedResponse> {
let request = Self::prepare_bond_request(
bond,
payout_address,
musig_data,
trader_setup,
taproot_pubkey,
)?;
// let request = Self::prepare_bond_request(
// bond,
// payout_address,
// musig_data,
// trader_setup,
// taproot_pubkey,
// )?;
let client = reqwest::blocking::Client::new();
let res = client
.post(format!(
"{}{}",
trader_setup.coordinator_endpoint, "/submit-maker-bond"
))
.json(&request)
.json(self)
.send();
match res {
Ok(res) => {

View File

@ -12,22 +12,28 @@ impl ActiveOffer {
trading_wallet.trade_onchain_assembly(&offer_conditions, maker_config)?;
let (psbt_inputs_hex_csv, escrow_change_address) =
trading_wallet.get_escrow_psbt_inputs()?;
trading_wallet.get_escrow_psbt_inputs(offer_conditions.locking_amount_sat as i64)?;
let submission_result = BondSubmissionRequest::send_maker(
&maker_config.robosats_robohash_hex,
&bond,
&mut musig_data,
&payout_address,
maker_config,
&trading_wallet.taproot_pubkey,
)?;
let bond_submission_request = BondSubmissionRequest {
robohash_hex: maker_config.robosats_robohash_hex.clone(),
signed_bond_hex: bond.to_string(),
payout_address: payout_address.address.to_string(),
musig_pub_nonce_hex: hex::encode(musig_data.nonce.get_pub_for_sharing()?.serialize()),
musig_pubkey_hex: hex::encode(musig_data.public_key.to_string()),
taproot_pubkey_hex: hex::encode(&trading_wallet.taproot_pubkey.serialize()),
bdk_psbt_inputs_hex_csv: psbt_inputs_hex_csv,
client_change_address: escrow_change_address,
};
let submission_result = bond_submission_request.send_maker(maker_config)?;
Ok(ActiveOffer {
offer_id_hex: submission_result.offer_id_hex,
used_musig_config: musig_data,
used_bond: bond,
expected_payout_address: payout_address,
escrow_psbt: None,
psbt_inputs_hex_csv,
escrow_change_address,
})
}

View File

@ -5,7 +5,7 @@ pub mod utils;
use self::utils::ActiveOffer;
use super::*;
use crate::{
cli::TraderSettings,
cli::{OfferType, TraderSettings},
communication::api::{
BondRequirementResponse, BondSubmissionRequest, IsOfferReadyRequest, OfferTakenRequest,
OfferTakenResponse, PsbtSubmissionRequest, PublicOffer, PublicOffers,
@ -31,9 +31,9 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> {
info!("Maker offer created: {:#?}", &offer);
let escrow_psbt_requirements = offer.wait_until_taken(maker_config)?;
let escrow_psbt = wallet.get_escrow_psbt(escrow_psbt_requirements, maker_config)?;
// .validate_maker_psbt(&escrow_contract_psbt)?
// .sign_escrow_psbt(&mut escrow_contract_psbt)?;
let escrow_psbt = wallet
.validate_maker_psbt(&escrow_contract_psbt)?
.sign_escrow_psbt(&mut escrow_contract_psbt)?;
// submit signed escrow psbt back to coordinator
PsbtSubmissionRequest::submit_escrow_psbt(

View File

@ -7,6 +7,8 @@ pub struct ActiveOffer {
pub used_bond: PartiallySignedTransaction,
pub expected_payout_address: AddressInfo,
pub escrow_psbt: Option<PartiallySignedTransaction>,
pub escrow_change_address: String,
pub psbt_inputs_hex_csv: String,
}
impl ActiveOffer {

View File

@ -12,6 +12,7 @@ use bdk::{
bitcoin::{
self,
bip32::ExtendedPrivKey,
consensus::encode::serialize_hex,
key::{KeyPair, Secp256k1, XOnlyPublicKey},
psbt::{serialize, Input, PartiallySignedTransaction},
Address, Network,
@ -38,6 +39,12 @@ pub struct TradingWallet {
pub taproot_pubkey: XOnlyPublicKey,
}
#[derive(Serialize)]
pub struct PsbtInput {
pub psbt_input: Input,
pub utxo: bdk::bitcoin::OutPoint,
}
pub fn get_wallet_xprv(xprv_input: Option<String>) -> Result<ExtendedPrivKey> {
let xprv: ExtendedPrivKey;
let network: Network = Network::Regtest;
@ -138,8 +145,12 @@ impl TradingWallet {
// could use more advanced coin selection if neccessary
for utxo in available_utxos {
let psbt_input = self.wallet.get_psbt_input(utxo, None, false)?;
inputs.push(hex::encode(bincode::serialize(&psbt_input)?));
let psbt_input: Input = self.wallet.get_psbt_input(utxo, None, false)?;
let input = PsbtInput {
psbt_input,
utxo: utxo.outpoint,
};
inputs.push(hex::encode(bincode::serialize(&input)?));
amount_sat -= utxo.txout.value as i64;
if amount_sat <= 0 {
break;