mirror of
https://github.com/RoboSats/taptrade-core.git
synced 2025-09-08 10:04:06 +00:00
finish keyspend psbt creation on coordinator, begin signing on client
This commit is contained in:
@ -91,6 +91,12 @@ pub struct PsbtSubmissionRequest {
|
||||
pub robohash_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PayoutResponse {
|
||||
pub payout_psbt_hex: String,
|
||||
pub agg_musig_nonce_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TradeObligationsUnsatisfied {
|
||||
pub robohash_hex: String,
|
||||
|
@ -21,6 +21,7 @@ pub enum FetchEscrowConfirmationError {
|
||||
pub enum RequestError {
|
||||
Database(String),
|
||||
NotConfirmed,
|
||||
CoordinatorError(String),
|
||||
NotFound,
|
||||
PsbtAlreadySubmitted,
|
||||
PsbtInvalid(String),
|
||||
|
@ -237,7 +237,9 @@ async fn poll_final_payout(
|
||||
match handle_final_payout(&payload, coordinator).await {
|
||||
Ok(PayoutProcessingResult::NotReady) => Ok(StatusCode::ACCEPTED.into_response()),
|
||||
Ok(PayoutProcessingResult::LostEscrow) => Ok(StatusCode::GONE.into_response()),
|
||||
Ok(PayoutProcessingResult::ReadyPSBT(psbt)) => Ok(psbt.into_response()),
|
||||
Ok(PayoutProcessingResult::ReadyPSBT(psbt_and_nonce)) => {
|
||||
Ok(Json(psbt_and_nonce).into_response())
|
||||
}
|
||||
Ok(PayoutProcessingResult::DecidingEscrow) => Ok(StatusCode::PROCESSING.into_response()),
|
||||
Err(RequestError::NotConfirmed) => {
|
||||
info!("Offer tx for final payout not confirmed");
|
||||
|
@ -1,13 +1,78 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use bdk::{
|
||||
bitcoin::{key::XOnlyPublicKey, Address},
|
||||
miniscript::Descriptor,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PayoutProcessingResult {
|
||||
ReadyPSBT(String),
|
||||
ReadyPSBT(PayoutResponse),
|
||||
NotReady,
|
||||
LostEscrow,
|
||||
DecidingEscrow,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayoutData {
|
||||
pub escrow_output_descriptor: Descriptor<XOnlyPublicKey>,
|
||||
pub payout_address_maker: Address,
|
||||
pub payout_address_taker: Address,
|
||||
pub payout_amount_maker: u64,
|
||||
pub payout_amount_taker: u64,
|
||||
pub agg_musig_nonce: MusigAggNonce,
|
||||
}
|
||||
|
||||
impl PayoutData {
|
||||
pub fn new_from_strings(
|
||||
escrow_output_descriptor: &str,
|
||||
payout_address_maker: &str,
|
||||
payout_address_taker: &str,
|
||||
payout_amount_maker: u64,
|
||||
payout_amount_taker: u64,
|
||||
musig_pub_nonce_hex_maker: &str,
|
||||
musig_pub_nonce_hex_taker: &str,
|
||||
) -> Result<Self> {
|
||||
let musig_pub_nonce_maker = match MusigPubNonce::from_hex(musig_pub_nonce_hex_maker) {
|
||||
Ok(musig_pub_nonce_maker) => musig_pub_nonce_maker,
|
||||
Err(e) => {
|
||||
return Err(anyhow!(
|
||||
"Error decoding maker musig pub nonce: {}",
|
||||
e.to_string()
|
||||
))
|
||||
}
|
||||
};
|
||||
let musig_pub_nonce_taker = match MusigPubNonce::from_hex(musig_pub_nonce_hex_taker) {
|
||||
Ok(musig_pub_nonce_taker) => musig_pub_nonce_taker,
|
||||
Err(e) => {
|
||||
return Err(anyhow!(
|
||||
"Error decoding taker musig pub nonce: {}",
|
||||
e.to_string()
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let agg_musig_nonce: MusigAggNonce =
|
||||
musig2::AggNonce::sum([musig_pub_nonce_maker, musig_pub_nonce_taker]);
|
||||
|
||||
Ok(Self {
|
||||
escrow_output_descriptor: Descriptor::from_str(escrow_output_descriptor)?,
|
||||
payout_address_maker: Address::from_str(payout_address_maker)?
|
||||
.require_network(bdk::bitcoin::Network::Regtest)
|
||||
.context("Maker payout address wrong network")?,
|
||||
payout_address_taker: Address::from_str(payout_address_taker)?
|
||||
.require_network(bdk::bitcoin::Network::Regtest)
|
||||
.context("Taker payout address wrong Network")?,
|
||||
payout_amount_maker,
|
||||
payout_amount_taker,
|
||||
agg_musig_nonce,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_random_order_id(len: usize) -> String {
|
||||
// Generate `len` random bytes
|
||||
let bytes: Vec<u8> = rand::thread_rng()
|
||||
|
@ -317,12 +317,23 @@ pub async fn handle_final_payout(
|
||||
if trader_happiness.maker_happy.is_some_and(|x| x)
|
||||
&& trader_happiness.taker_happy.is_some_and(|x| x)
|
||||
{
|
||||
panic!("Implement wallet.assemble_keyspend_payout_psbt()");
|
||||
// let payout_keyspend_psbt_hex = wallet
|
||||
// .assemble_keyspend_payout_psbt(&payload.offer_id_hex, &payload.robohash_hex)
|
||||
// .await
|
||||
// .context("Error assembling payout PSBT")?;
|
||||
// return Ok(PayoutProcessingResult::ReadyPSBT(payout_keyspend_psbt_hex));
|
||||
let escrow_payout_data = match database.fetch_payout_data(&payload.offer_id_hex).await {
|
||||
Ok(payout_data) => payout_data,
|
||||
Err(e) => return Err(RequestError::Database(e.to_string())),
|
||||
};
|
||||
|
||||
let payout_keyspend_psbt_hex = match coordinator
|
||||
.coordinator_wallet
|
||||
.assemble_keyspend_payout_psbt(&escrow_payout_data)
|
||||
.await
|
||||
{
|
||||
Ok(psbt_hex) => psbt_hex,
|
||||
Err(e) => return Err(RequestError::CoordinatorError(e.to_string())),
|
||||
};
|
||||
return Ok(PayoutProcessingResult::ReadyPSBT(PayoutResponse {
|
||||
payout_psbt_hex: payout_keyspend_psbt_hex,
|
||||
agg_musig_nonce_hex: escrow_payout_data.agg_musig_nonce.to_string(),
|
||||
}));
|
||||
} else if (trader_happiness.maker_happy.is_none() || trader_happiness.taker_happy.is_none())
|
||||
&& !trader_happiness.escrow_ongoing
|
||||
{
|
||||
|
@ -853,4 +853,35 @@ impl CoordinatorDB {
|
||||
musig_pubkey_compressed_hex: row.get("musig_pubkey_hex"),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_payout_data(&self, trade_id: &str) -> Result<PayoutData> {
|
||||
let row = sqlx::query(
|
||||
"SELECT escrow_output_descriptor, payout_address_maker,
|
||||
payout_address_taker, musig_pub_nonce_hex_maker, musig_pub_nonce_hex_taker,
|
||||
escrow_amount_maker_sat, escrow_amount_taker_sat
|
||||
FROM taken_offers WHERE offer_id = ?",
|
||||
)
|
||||
.bind(trade_id)
|
||||
.fetch_one(&*self.db_pool)
|
||||
.await
|
||||
.context("SQL query to fetch escrow_ouput_descriptor failed.")?;
|
||||
|
||||
let escrow_output_descriptor = row.try_get("escrow_output_descriptor")?;
|
||||
let payout_address_maker = row.try_get("payout_address_maker")?;
|
||||
let payout_address_taker = row.try_get("payout_address_taker")?;
|
||||
let musig_pub_nonce_hex_maker: &str = row.try_get("musig_pub_nonce_hex_maker")?;
|
||||
let musig_pub_nonce_hex_taker: &str = row.try_get("musig_pub_nonce_hex_taker")?;
|
||||
let payout_amount_maker: u64 = row.try_get::<i64, _>("escrow_amount_maker_sat")? as u64;
|
||||
let payout_amount_taker: u64 = row.try_get::<i64, _>("escrow_amount_taker_sat")? as u64;
|
||||
|
||||
PayoutData::new_from_strings(
|
||||
escrow_output_descriptor,
|
||||
payout_address_maker,
|
||||
payout_address_taker,
|
||||
payout_amount_maker,
|
||||
payout_amount_taker,
|
||||
musig_pub_nonce_hex_maker,
|
||||
musig_pub_nonce_hex_taker,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ use coordinator::{
|
||||
use database::CoordinatorDB;
|
||||
use dotenvy::dotenv;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use musig2::{AggNonce as MusigAggNonce, PubNonce as MusigPubNonce};
|
||||
use rand::Rng;
|
||||
use std::{
|
||||
env,
|
||||
|
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use axum::routing::trace;
|
||||
use bdk::{
|
||||
bitcoin::{psbt::PartiallySignedTransaction, PublicKey},
|
||||
descriptor::{policy, Descriptor},
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::ops::Add;
|
||||
|
||||
/// construction of the transaction spending the escrow output after a successfull trade as keyspend transaction
|
||||
use super::*;
|
||||
use bdk::bitcoin::psbt::Input;
|
||||
use bdk::bitcoin::psbt::PartiallySignedTransaction;
|
||||
use bdk::bitcoin::OutPoint;
|
||||
use bdk::miniscript::Descriptor;
|
||||
|
||||
fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> {
|
||||
let feerate = blockchain_backend.estimate_fee(6)?;
|
||||
@ -15,24 +15,56 @@ fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)>
|
||||
}
|
||||
|
||||
impl<D: bdk::database::BatchDatabase> CoordinatorWallet<D> {
|
||||
fn get_escrow_utxo(
|
||||
&self,
|
||||
descriptor: &Descriptor<XOnlyPublicKey>,
|
||||
) -> anyhow::Result<(Input, OutPoint)> {
|
||||
let temp_wallet = Wallet::new(
|
||||
&descriptor.to_string(),
|
||||
None,
|
||||
bdk::bitcoin::Network::Regtest,
|
||||
MemoryDatabase::new(),
|
||||
)?;
|
||||
temp_wallet.sync(&self.backend, SyncOptions::default())?;
|
||||
let available_utxos = temp_wallet.list_unspent()?;
|
||||
if available_utxos.len() != 1 {
|
||||
return Err(anyhow!("Expected exactly one utxo for escrow output"));
|
||||
};
|
||||
|
||||
let input = temp_wallet.get_psbt_input(available_utxos[0].clone(), None, false)?;
|
||||
let outpoint = available_utxos[0].outpoint;
|
||||
Ok((input, outpoint))
|
||||
}
|
||||
|
||||
pub async fn assemble_keyspend_payout_psbt(
|
||||
&self,
|
||||
escrow_output: OutPoint,
|
||||
payout_addresses: HashMap<Address, u64>,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
payout_information: &PayoutData,
|
||||
) -> anyhow::Result<String> {
|
||||
let (escrow_utxo_psbt_input, escrow_utxo_outpoint) =
|
||||
self.get_escrow_utxo(&payout_information.escrow_output_descriptor)?;
|
||||
|
||||
let (payout_psbt, _) = {
|
||||
let wallet = self.wallet.lock().await;
|
||||
let mut builder = wallet.build_tx();
|
||||
let (tx_fee_abs, tx_fee_abs_sat_per_user) = get_tx_fees_abs_sat(&self.backend)?;
|
||||
|
||||
builder.add_utxo(escrow_output)?;
|
||||
for (address, amount) in payout_addresses {
|
||||
builder.add_recipient(address.script_pubkey(), amount - tx_fee_abs_sat_per_user);
|
||||
}
|
||||
// why 264 wu?: see escrow_psbt.tx
|
||||
builder.add_foreign_utxo(escrow_utxo_outpoint, escrow_utxo_psbt_input, 264)?;
|
||||
|
||||
builder.add_recipient(
|
||||
payout_information.payout_address_maker.script_pubkey(),
|
||||
payout_information.payout_amount_maker - tx_fee_abs_sat_per_user,
|
||||
);
|
||||
|
||||
builder.add_recipient(
|
||||
payout_information.payout_address_taker.script_pubkey(),
|
||||
payout_information.payout_amount_taker - tx_fee_abs_sat_per_user,
|
||||
);
|
||||
|
||||
builder.fee_absolute(tx_fee_abs);
|
||||
|
||||
builder.finish()?
|
||||
};
|
||||
Ok(payout_psbt)
|
||||
Ok(payout_psbt.serialize_hex())
|
||||
}
|
||||
}
|
||||
|
@ -110,11 +110,11 @@ pub struct TradeObligationsSatisfied {
|
||||
pub offer_id_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PayoutPsbtResponse {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PayoutResponse {
|
||||
pub payout_psbt_hex: String,
|
||||
pub agg_musig_nonce_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TradeObligationsUnsatisfied {
|
||||
pub robohash_hex: String,
|
||||
|
@ -14,6 +14,7 @@ use bdk::{
|
||||
bitcoin::{consensus::Encodable, psbt::PartiallySignedTransaction},
|
||||
wallet::AddressInfo,
|
||||
};
|
||||
use musig2::AggNonce;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{f32::consts::E, str::FromStr, thread::sleep, time::Duration};
|
||||
|
||||
@ -247,7 +248,7 @@ impl IsOfferReadyRequest {
|
||||
pub fn poll_payout(
|
||||
trader_config: &TraderSettings,
|
||||
offer: &ActiveOffer,
|
||||
) -> Result<Option<PartiallySignedTransaction>> {
|
||||
) -> Result<(PartiallySignedTransaction, AggNonce)> {
|
||||
let request = IsOfferReadyRequest {
|
||||
robohash_hex: trader_config.robosats_robohash_hex.clone(),
|
||||
offer_id_hex: offer.offer_id_hex.clone(),
|
||||
@ -290,10 +291,11 @@ impl IsOfferReadyRequest {
|
||||
));
|
||||
}
|
||||
}
|
||||
let final_psbt = PartiallySignedTransaction::from_str(
|
||||
&res.json::<PayoutPsbtResponse>()?.payout_psbt_hex,
|
||||
)?;
|
||||
Ok(Some(final_psbt))
|
||||
let payout_response: PayoutResponse = res.json()?;
|
||||
let final_psbt = PartiallySignedTransaction::from_str(&payout_response.payout_psbt_hex)?;
|
||||
let agg_nonce = AggNonce::from_str(&payout_response.agg_musig_nonce_hex)
|
||||
.map_err(|e| anyhow!("Error parsing agg nonce: {}", e))?;
|
||||
Ok((final_psbt, agg_nonce))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,13 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> {
|
||||
// this represents the "confirm payment" / "confirm fiat recieved" button
|
||||
TradeObligationsSatisfied::submit(&offer.offer_id_hex, maker_config)?;
|
||||
info!("Waiting for other party to confirm the trade.");
|
||||
let payout_keyspend_psbt = IsOfferReadyRequest::poll_payout(maker_config, &offer)?;
|
||||
let (payout_keyspend_psbt, agg_pub_nonce) =
|
||||
IsOfferReadyRequest::poll_payout(maker_config, &offer)?;
|
||||
let signed_payout_psbt = wallet
|
||||
.validate_payout_psbt(&payout_keyspend_psbt)?
|
||||
.sign_payout_psbt(payout_keyspend_psbt, agg_pub_nonce)?;
|
||||
// submit signed payout psbt back to coordinator
|
||||
panic!("Payout to be implemented!");
|
||||
} else {
|
||||
error!("Trade failed. Initiating escrow mode.");
|
||||
TradeObligationsUnsatisfied::request_escrow(&offer.offer_id_hex, maker_config)?;
|
||||
|
@ -7,6 +7,7 @@ use crate::{
|
||||
cli::TraderSettings,
|
||||
communication::api::{BondRequirementResponse, OfferTakenResponse},
|
||||
};
|
||||
use ::musig2::AggNonce;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bdk::{
|
||||
bitcoin::{
|
||||
@ -197,4 +198,20 @@ impl TradingWallet {
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn validate_payout_psbt(&self, psbt: &PartiallySignedTransaction) -> Result<&Self> {
|
||||
warn!("IMPLEMENT PAYOUT PSBT VALIDATION for production use!");
|
||||
// validate: change output address, amounts, fee
|
||||
// tbd once the trade psbt is implemented on coordinator side
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn sign_payout_psbt(
|
||||
&self,
|
||||
psbt: PartiallySignedTransaction,
|
||||
agg_pub_nonce: AggNonce,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
Ok(signed_psbt)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user