mirror of
https://github.com/RoboSats/taptrade-core.git
synced 2025-12-25 22:29:19 +00:00
submit taker bond database function
This commit is contained in:
@ -47,9 +47,23 @@ pub struct OffersRequest {
|
||||
pub struct PublicOffer {
|
||||
pub amount_sat: u64,
|
||||
pub offer_id_hex: String,
|
||||
pub required_bond_amount_sat: u64,
|
||||
pub bond_locking_address: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct PublicOffers {
|
||||
pub offers: Option<Vec<PublicOffer>>, // don't include offers var in return json if no offers are available
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Deserialize)]
|
||||
pub struct OfferTakenResponse {
|
||||
pub trade_psbt_hex_to_sign: String,
|
||||
}
|
||||
|
||||
// request to receive the escrow psbt to sign for the specified offer to take it
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OfferPsbtRequest {
|
||||
pub offer: PublicOffer,
|
||||
pub trade_data: BondSubmissionRequest,
|
||||
}
|
||||
|
||||
@ -44,18 +44,22 @@ async fn submit_maker_bond(
|
||||
Extension(database): Extension<CoordinatorDB>,
|
||||
Extension(wallet): Extension<CoordinatorWallet>,
|
||||
Json(payload): Json<BondSubmissionRequest>,
|
||||
) -> Result<Json<OrderActivatedResponse>, AppError> {
|
||||
) -> Result<Response, AppError> {
|
||||
let bond_requirements = database.fetch_maker_request(&payload.robohash_hex).await?;
|
||||
let offer_id_hex = generate_random_order_id(16); // 16 bytes random offer id, maybe a different system makes more sense later on? (uuid or increasing counter...)
|
||||
|
||||
// validate bond (check amounts, valid inputs, correct addresses, valid signature, feerate)
|
||||
wallet
|
||||
if !wallet
|
||||
.validate_bond_tx_hex(&payload.signed_bond_hex, &bond_requirements)
|
||||
.await?;
|
||||
|
||||
.await?
|
||||
{
|
||||
return Ok(StatusCode::NOT_ACCEPTABLE.into_response());
|
||||
}
|
||||
let offer_id_hex = generate_random_order_id(16); // 16 bytes random offer id, maybe a different system makes more sense later on? (uuid or increasing counter...)
|
||||
// create address for taker bond
|
||||
let new_taker_bond_address = wallet.get_new_address().await?;
|
||||
// insert bond into sql database and move offer to different table
|
||||
let bond_locked_until_timestamp = database
|
||||
.move_offer_to_active(&payload, &offer_id_hex)
|
||||
.move_offer_to_active(&payload, &offer_id_hex, new_taker_bond_address)
|
||||
.await?;
|
||||
|
||||
// begin monitoring bond -> async loop monitoring bonds in sql table "active_maker_offers" -> see ../coordinator/monitoring.rs
|
||||
@ -65,7 +69,8 @@ async fn submit_maker_bond(
|
||||
Ok(Json(OrderActivatedResponse {
|
||||
bond_locked_until_timestamp,
|
||||
offer_id_hex,
|
||||
}))
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn fetch_available_offers(
|
||||
@ -77,11 +82,38 @@ async fn fetch_available_offers(
|
||||
Ok(Json(PublicOffers { offers }))
|
||||
}
|
||||
|
||||
async fn submit_taker_bond(
|
||||
Extension(database): Extension<CoordinatorDB>,
|
||||
Extension(wallet): Extension<CoordinatorWallet>,
|
||||
Json(payload): Json<OfferPsbtRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let bond_requirements = database
|
||||
.fetch_taker_bond_requirements(&payload.offer.offer_id_hex)
|
||||
.await;
|
||||
match bond_requirements {
|
||||
Ok(bond_requirements) => {
|
||||
if !wallet
|
||||
.validate_bond_tx_hex(&payload.trade_data.signed_bond_hex, &bond_requirements)
|
||||
.await?
|
||||
{
|
||||
dbg!("Taker Bond validation failed");
|
||||
return Ok(StatusCode::NOT_ACCEPTABLE.into_response());
|
||||
}
|
||||
}
|
||||
Err(_) => return Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
}
|
||||
database.add_taker_info_and_move_table(&payload).await?;
|
||||
Ok(Json(OfferTakenResponse { trade_psbt_hex }).into_response())
|
||||
}
|
||||
|
||||
pub async fn api_server(database: CoordinatorDB, wallet: CoordinatorWallet) -> Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/create-offer", post(receive_order))
|
||||
.route("/submit-maker-bond", post(submit_maker_bond))
|
||||
.route("/fetch-available-offers", post(fetch_available_offers))
|
||||
.route("/submit-taker-bond", post(submit_taker_bond))
|
||||
// submit-taker-bond
|
||||
// request-offer-status
|
||||
.layer(Extension(database))
|
||||
.layer(Extension(wallet));
|
||||
// add other routes here
|
||||
|
||||
@ -10,14 +10,29 @@ pub struct CoordinatorDB {
|
||||
}
|
||||
|
||||
// db structure of offers awaiting bond submission in table maker_requests
|
||||
pub struct AwaitingBondOffer {
|
||||
pub robohash_hex: String,
|
||||
pub is_buy_order: bool,
|
||||
pub amount_satoshi: u64,
|
||||
pub bond_ratio: u8,
|
||||
pub offer_duration_ts: u64,
|
||||
pub bond_address: String,
|
||||
pub bond_amount_sat: u64,
|
||||
struct AwaitingBondOffer {
|
||||
robohash_hex: String,
|
||||
is_buy_order: bool,
|
||||
amount_satoshi: u64,
|
||||
bond_ratio: u8,
|
||||
offer_duration_ts: u64,
|
||||
bond_address: String,
|
||||
bond_amount_sat: u64,
|
||||
}
|
||||
|
||||
struct AwaitinigTakerOffer {
|
||||
offer_id: String,
|
||||
robohash_maker: Vec<u8>,
|
||||
is_buy_order: bool,
|
||||
amount_sat: i64,
|
||||
bond_ratio: i32,
|
||||
offer_duration_ts: i64,
|
||||
bond_address_maker: String,
|
||||
bond_amount_sat: i64,
|
||||
bond_tx_hex_maker: String,
|
||||
payout_address_maker: String,
|
||||
musig_pub_nonce_hex_maker: String,
|
||||
musig_pubkey_hex_maker: String,
|
||||
}
|
||||
|
||||
// is our implementation resistant against sql injections?
|
||||
@ -70,6 +85,33 @@ impl CoordinatorDB {
|
||||
)
|
||||
.execute(&db_pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS taken_offers (
|
||||
offer_id TEXT PRIMARY KEY,
|
||||
robohash_maker BLOB,
|
||||
robohash_taker BLOB,
|
||||
is_buy_order INTEGER,
|
||||
amount_sat INTEGER NOT NULL,
|
||||
bond_ratio INTEGER NOT NULL,
|
||||
offer_duration_ts INTEGER NOT NULL,
|
||||
bond_address_maker TEXT NOT NULL,
|
||||
bond_address_taker TEXT NOT NULL,
|
||||
bond_amount_sat INTEGER NOT NULL,
|
||||
bond_tx_hex_maker TEXT NOT NULL,
|
||||
bond_tx_hex_taker TEXT NOT NULL,
|
||||
payout_address_maker TEXT NOT NULL,
|
||||
payout_address_taker TEXT NOT NULL,
|
||||
musig_pub_nonce_hex_maker TEXT NOT NULL,
|
||||
musig_pubkey_hex_maker TEXT NOT NULL,
|
||||
musig_pub_nonce_hex_taker TEXT NOT NULL,
|
||||
musig_pubkey_hex_taker TEXT NOT NULL,
|
||||
escrow_psbt_hex_maker TEXT,
|
||||
escrow_psbt_hex_taker TEXT
|
||||
)",
|
||||
)
|
||||
.execute(&db_pool)
|
||||
.await?;
|
||||
dbg!("Database initialized");
|
||||
let shared_db_pool = Arc::new(db_pool);
|
||||
Ok(Self {
|
||||
@ -120,7 +162,7 @@ impl CoordinatorDB {
|
||||
&self,
|
||||
robohash_hex: &str,
|
||||
) -> Result<AwaitingBondOffer> {
|
||||
let fetched_values = sqlx::query_as::<_, (String, bool, i64, u8, i64, String, i64)> (
|
||||
let fetched_values = sqlx::query_as::<_, (Vec<u8>, bool, i64, u8, i64, String, i64)> (
|
||||
"SELECT robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat FROM maker_requests WHERE <unique_identifier_column> = ?",
|
||||
)
|
||||
.bind(hex::decode(robohash_hex)?)
|
||||
@ -148,6 +190,7 @@ impl CoordinatorDB {
|
||||
&self,
|
||||
data: &BondSubmissionRequest,
|
||||
offer_id: &String,
|
||||
taker_bond_address: String,
|
||||
) -> Result<u64> {
|
||||
let remaining_offer_information = self
|
||||
.fetch_and_delete_offer_from_bond_table(&data.robohash_hex)
|
||||
@ -156,7 +199,7 @@ impl CoordinatorDB {
|
||||
sqlx::query(
|
||||
"INSERT OR REPLACE INTO active_maker_offers (offer_id, robohash, is_buy_order, amount_sat,
|
||||
bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, musig_pub_nonce_hex, musig_pubkey_hex)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(offer_id)
|
||||
.bind(hex::decode(&data.robohash_hex)?)
|
||||
@ -170,6 +213,7 @@ impl CoordinatorDB {
|
||||
.bind(data.payout_address.clone())
|
||||
.bind(data.musig_pub_nonce_hex.clone())
|
||||
.bind(data.musig_pubkey_hex.clone())
|
||||
.bind(taker_bond_address)
|
||||
.execute(&*self.db_pool)
|
||||
.await?;
|
||||
|
||||
@ -180,8 +224,8 @@ impl CoordinatorDB {
|
||||
&self,
|
||||
requested_offer: &OffersRequest,
|
||||
) -> Result<Option<Vec<PublicOffer>>> {
|
||||
let fetched_offers = sqlx::query_as::<_, (String, i64)> (
|
||||
"SELECT offer_id, amount_sat FROM maker_requests WHERE is_buy_order = ? AND amount_sat BETWEEN ? AND ?",
|
||||
let fetched_offers = sqlx::query_as::<_, (String, i64, i64, String)> (
|
||||
"SELECT offer_id, amount_sat, bond_amount_sat, taker_bond_address FROM active_maker_offers WHERE is_buy_order = ? AND amount_sat BETWEEN ? AND ?",
|
||||
)
|
||||
.bind(requested_offer.buy_offers)
|
||||
.bind(requested_offer.amount_min_sat as i64)
|
||||
@ -191,16 +235,114 @@ impl CoordinatorDB {
|
||||
|
||||
let available_offers: Vec<PublicOffer> = fetched_offers
|
||||
.into_iter()
|
||||
.map(|(offer_id_hex, amount_sat)| PublicOffer {
|
||||
offer_id_hex,
|
||||
amount_sat: amount_sat as u64,
|
||||
})
|
||||
.map(
|
||||
|(offer_id_hex, amount_sat, bond_amount_sat, bond_address_taker)| PublicOffer {
|
||||
offer_id_hex,
|
||||
amount_sat: amount_sat as u64,
|
||||
required_bond_amount_sat: bond_amount_sat as u64,
|
||||
bond_locking_address: bond_address_taker,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
if available_offers.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(available_offers))
|
||||
}
|
||||
|
||||
pub async fn fetch_taker_bond_requirements(
|
||||
&self,
|
||||
offer_id_hex: &String,
|
||||
) -> Result<BondRequirementResponse> {
|
||||
let taker_bond_requirements = sqlx::query(
|
||||
"SELECT taker_bond_address, bond_amount_sat FROM active_maker_offers WHERE offer_id = ?",
|
||||
)
|
||||
.bind(offer_id_hex)
|
||||
.fetch_one(&*self.db_pool)
|
||||
.await?;
|
||||
|
||||
Ok(BondRequirementResponse {
|
||||
bond_address: taker_bond_requirements.try_get("taker_bond_address")?,
|
||||
locking_amount_sat: taker_bond_requirements.try_get::<i64, _>("bond_amount_sat")?
|
||||
as u64,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_and_delete_offer_from_public_offers_table(
|
||||
&self,
|
||||
offer_id_hex: &str,
|
||||
) -> Result<AwaitinigTakerOffer> {
|
||||
let fetched_values = sqlx::query_as::<_, (Vec<u8>, bool, i64, i32, i64, String, i64, String, String, String, String)> (
|
||||
"SELECT robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address,
|
||||
musig_pub_nonce_hex, musig_pubkey_hex FROM active_maker_offers WHERE <unique_identifier_column> = ?",
|
||||
)
|
||||
.bind(offer_id_hex)
|
||||
.fetch_one(&*self.db_pool)
|
||||
.await?;
|
||||
|
||||
// Delete the database entry.
|
||||
sqlx::query("DELETE FROM active_maker_offers WHERE <unique_identifier_column> = ?")
|
||||
.bind(offer_id_hex)
|
||||
.execute(&*self.db_pool)
|
||||
.await?;
|
||||
|
||||
Ok(AwaitinigTakerOffer {
|
||||
offer_id: offer_id_hex.to_string(),
|
||||
robohash_maker: fetched_values.0,
|
||||
is_buy_order: fetched_values.1,
|
||||
amount_sat: fetched_values.2,
|
||||
bond_ratio: fetched_values.3,
|
||||
offer_duration_ts: fetched_values.4,
|
||||
bond_address_maker: fetched_values.5,
|
||||
bond_amount_sat: fetched_values.6,
|
||||
bond_tx_hex_maker: fetched_values.7,
|
||||
payout_address_maker: fetched_values.8,
|
||||
musig_pub_nonce_hex_maker: fetched_values.9,
|
||||
musig_pubkey_hex_maker: fetched_values.10,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn add_taker_info_and_move_table(
|
||||
&self,
|
||||
trade_and_taker_info: &OfferPsbtRequest,
|
||||
) -> Result<()> {
|
||||
let public_offer = self
|
||||
.fetch_and_delete_offer_from_public_offers_table(
|
||||
&trade_and_taker_info.offer.offer_id_hex,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT OR REPLACE INTO taken_offers (offer_id, robohash_maker, robohash_taker, is_buy_order, amount_sat,
|
||||
bond_ratio, offer_duration_ts, bond_address_maker, bond_address_taker, bond_amount_sat, bond_tx_hex_maker,
|
||||
bond_tx_hex_taker, payout_address_maker, payout_address_taker, musig_pub_nonce_hex_maker, musig_pubkey_hex_maker
|
||||
musig_pub_nonce_hex_taker, musig_pubkey_hex_taker, escrow_psbt_hex_maker, escrow_psbt_hex_taker)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(public_offer.offer_id)
|
||||
.bind(public_offer.robohash_maker)
|
||||
.bind(hex::decode(&trade_and_taker_info.trade_data.robohash_hex)?)
|
||||
.bind(public_offer.is_buy_order)
|
||||
.bind(public_offer.amount_sat)
|
||||
.bind(public_offer.bond_ratio)
|
||||
.bind(public_offer.offer_duration_ts)
|
||||
.bind(public_offer.bond_address_maker)
|
||||
.bind(trade_and_taker_info.offer.bond_locking_address.clone())
|
||||
.bind(public_offer.bond_amount_sat)
|
||||
.bind(public_offer.bond_tx_hex_maker)
|
||||
.bind(trade_and_taker_info.trade_data.signed_bond_hex.clone())
|
||||
.bind(public_offer.payout_address_maker)
|
||||
.bind(trade_and_taker_info.trade_data.payout_address.clone())
|
||||
.bind(public_offer.musig_pub_nonce_hex_maker)
|
||||
.bind(public_offer.musig_pubkey_hex_maker)
|
||||
.bind(trade_and_taker_info.trade_data.musig_pub_nonce_hex.clone())
|
||||
.bind(trade_and_taker_info.trade_data.musig_pubkey_hex.clone())
|
||||
.bind(escrow psbt maker)
|
||||
.bind(escrow psbt taker)
|
||||
.execute(&*self.db_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -69,6 +69,8 @@ pub struct PublicOffers {
|
||||
pub struct PublicOffer {
|
||||
pub amount_sat: u64,
|
||||
pub offer_id_hex: String,
|
||||
pub required_bond_amount_sat: u64,
|
||||
pub bond_locking_address: String,
|
||||
}
|
||||
|
||||
// request to receive the escrow psbt to sign for the specified offer to take it
|
||||
|
||||
@ -42,21 +42,6 @@ impl PublicOffers {
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicOffer {
|
||||
pub fn request_bond(&self, taker_config: &TraderSettings) -> Result<BondRequirementResponse> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}{}",
|
||||
taker_config.coordinator_endpoint, "/request-taker-bond"
|
||||
))
|
||||
.json(self)
|
||||
.send()?
|
||||
.json::<BondRequirementResponse>()?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl OfferPsbtRequest {
|
||||
pub fn taker_request(
|
||||
offer: &PublicOffer,
|
||||
|
||||
@ -11,12 +11,14 @@ impl ActiveOffer {
|
||||
taker_config: &TraderSettings,
|
||||
offer: &PublicOffer,
|
||||
) -> Result<ActiveOffer> {
|
||||
// fetching the bond requirements for the requested Offer (amount, locking address)
|
||||
let bond_conditions: BondRequirementResponse = offer.request_bond(taker_config)?;
|
||||
let bond_requirements = BondRequirementResponse {
|
||||
bond_address: offer.bond_locking_address.clone(),
|
||||
locking_amount_sat: offer.required_bond_amount_sat,
|
||||
};
|
||||
|
||||
// assembly of the Bond transaction and generation of MuSig data and payout address
|
||||
let (bond, mut musig_data, payout_address) =
|
||||
trading_wallet.trade_onchain_assembly(&bond_conditions, taker_config)?;
|
||||
trading_wallet.trade_onchain_assembly(&bond_requirements, taker_config)?;
|
||||
|
||||
// now we submit the signed bond transaction to the coordinator and receive the escrow PSBT we have to sign
|
||||
// in exchange
|
||||
|
||||
Reference in New Issue
Block a user