mirror of
https://github.com/RoboSats/taptrade-core.git
synced 2025-07-22 02:33:22 +00:00
Merge branch 'change-testing' into research
cleanup of webserver and separation of logic
This commit is contained in:
102
taptrade-cli-demo/coordinator/Cargo.lock
generated
102
taptrade-cli-demo/coordinator/Cargo.lock
generated
@ -472,6 +472,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -583,6 +584,41 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -1138,6 +1174,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@ -1611,6 +1653,30 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
@ -2368,6 +2434,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@ -2711,6 +2783,36 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"once_cell",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
@ -25,6 +25,7 @@ tower = "0.4.13"
|
||||
log = "0.4.22"
|
||||
env_logger = "0.11.3"
|
||||
sha2 = "0.10.8"
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
@ -1,17 +1,20 @@
|
||||
use super::*;
|
||||
|
||||
// Receiving this struct as input to the server
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct OrderRequest {
|
||||
pub robohash_hex: String, // identifier of the trader
|
||||
pub amount_satoshi: u64, // amount in satoshi to buy or sell
|
||||
pub is_buy_order: bool, // true if buy, false if sell
|
||||
pub bond_ratio: u8, // [2, 50]% of trading amount
|
||||
#[derive(Deserialize, Serialize, Debug, Validate)]
|
||||
pub struct OfferRequest {
|
||||
pub robohash_hex: String, // identifier of the trader
|
||||
#[validate(range(min = 10000, max = 20000000))]
|
||||
pub amount_satoshi: u64, // amount in satoshi to buy or sell
|
||||
pub is_buy_order: bool, // true if buy, false if sell
|
||||
#[validate(range(min = 2, max = 50))]
|
||||
pub bond_ratio: u8, // [2, 50]% of trading amount
|
||||
#[validate(custom(function = "validate_timestamp"))]
|
||||
pub offer_duration_ts: u64, // unix timestamp how long the offer should stay available
|
||||
}
|
||||
|
||||
// Define a struct representing your response data
|
||||
#[derive(Serialize, PartialEq, Debug)]
|
||||
#[derive(Serialize, PartialEq, Debug, Validate)]
|
||||
pub struct BondRequirementResponse {
|
||||
pub bond_address: String,
|
||||
pub locking_amount_sat: u64, // min amount of the bond output in sat
|
||||
@ -30,7 +33,7 @@ pub struct BondSubmissionRequest {
|
||||
|
||||
// Response after step2 if offer creation was successful and the offer is now online in the orderbook
|
||||
#[derive(Serialize)]
|
||||
pub struct OrderActivatedResponse {
|
||||
pub struct OfferActivatedResponse {
|
||||
pub offer_id_hex: String,
|
||||
pub bond_locked_until_timestamp: u64, // unix timestamp. Do not touch bond till then unless offer gets taken.
|
||||
}
|
||||
|
@ -5,14 +5,12 @@ use self::api::*;
|
||||
use self::utils::*;
|
||||
use super::*;
|
||||
use crate::wallet::*;
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
@ -22,152 +20,97 @@ use tokio::net::TcpListener;
|
||||
//
|
||||
/// Handler function to process the received data
|
||||
async fn receive_order(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(wallet): Extension<Arc<CoordinatorWallet<sled::Tree>>>,
|
||||
Json(order): Json<OrderRequest>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
Json(offer): Json<OfferRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
debug!("{:#?}", &order);
|
||||
if order.sanity_check().is_err() {
|
||||
warn!("Received order failed sanity check");
|
||||
return Ok(StatusCode::NOT_ACCEPTABLE.into_response());
|
||||
if let Err(_) = offer.validate() {
|
||||
return Ok(StatusCode::BAD_REQUEST.into_response());
|
||||
} else {
|
||||
let bond_requirements = process_order(coordinator, &offer).await?;
|
||||
return Ok(Json(bond_requirements).into_response());
|
||||
}
|
||||
let bond_requirements = BondRequirementResponse {
|
||||
bond_address: wallet.get_new_address().await?,
|
||||
locking_amount_sat: order.amount_satoshi * order.bond_ratio as u64 / 100,
|
||||
};
|
||||
// insert offer into sql database
|
||||
database
|
||||
.insert_new_maker_request(&order, &bond_requirements)
|
||||
.await?;
|
||||
debug!("Coordinator received new offer: {:?}", order);
|
||||
Ok(Json(bond_requirements).into_response())
|
||||
}
|
||||
|
||||
/// receives the maker bond, verifies it and moves to offer to the active table (orderbook)
|
||||
async fn submit_maker_bond(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(wallet): Extension<Arc<CoordinatorWallet<sled::Tree>>>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
Json(payload): Json<BondSubmissionRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
debug!("\n\nReceived maker bond: {:?}", payload);
|
||||
let bond_requirements = if let Ok(requirements) = database
|
||||
.fetch_bond_requirements(&payload.robohash_hex)
|
||||
.await
|
||||
{
|
||||
requirements
|
||||
} else {
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
};
|
||||
|
||||
match wallet
|
||||
.validate_bond_tx_hex(&payload.signed_bond_hex, &bond_requirements)
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
match handle_maker_bond(&payload, coordinator).await {
|
||||
Ok(offer_activated_response) => Ok(Json(offer_activated_response).into_response()),
|
||||
Err(BondError::BondNotFound) => {
|
||||
info!("Bond requirements not found in database");
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
}
|
||||
Err(BondError::InvalidBond(e)) => {
|
||||
warn!("Invalid bond submission: {e}");
|
||||
return Ok(StatusCode::NOT_ACCEPTABLE.into_response());
|
||||
}
|
||||
}
|
||||
debug!("\nBond validation successful");
|
||||
let offer_id_hex: String = 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.context(format!(
|
||||
"Error generating taker bond address for offer id: {}",
|
||||
offer_id_hex
|
||||
))?;
|
||||
// insert bond into sql database and move offer to different table
|
||||
let bond_locked_until_timestamp = match database
|
||||
.move_offer_to_active(&payload, &offer_id_hex, new_taker_bond_address)
|
||||
.await
|
||||
{
|
||||
Ok(timestamp) => timestamp,
|
||||
Err(e) => {
|
||||
debug!("Error in validate_bond_tx_hex: {}", e);
|
||||
Err(BondError::CoordinatorError(e)) => {
|
||||
error!("Coordinator error on bond submission: {e}");
|
||||
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
// Create the JSON response
|
||||
Ok(Json(OrderActivatedResponse {
|
||||
bond_locked_until_timestamp,
|
||||
offer_id_hex,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
}
|
||||
|
||||
/// returns available offers from the active table (orderbook)
|
||||
async fn fetch_available_offers(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
Json(payload): Json<OffersRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let offers: Option<Vec<PublicOffer>> = database.fetch_suitable_offers(&payload).await?;
|
||||
if offers.is_none() {
|
||||
return Ok(StatusCode::NO_CONTENT.into_response());
|
||||
debug!("\n\nReceived offer request: {:?}", payload);
|
||||
|
||||
match get_public_offers(&payload, coordinator).await {
|
||||
Ok(offers) => Ok(Json(offers).into_response()),
|
||||
Err(FetchOffersError::NoOffersAvailable) => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
Err(FetchOffersError::DatabaseError(e)) => {
|
||||
error!("Database error fetching offers: {e}");
|
||||
Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
|
||||
}
|
||||
}
|
||||
Ok(Json(PublicOffers { offers }).into_response())
|
||||
}
|
||||
|
||||
/// receives the taker bond for a given offer, verifies it, creates escrow transaction psbt
|
||||
/// and moves the offer to the taken table. Will return the trade contract psbt for the taker to sign.
|
||||
async fn submit_taker_bond(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(wallet): Extension<Arc<CoordinatorWallet<sled::Tree>>>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
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) => {
|
||||
match wallet
|
||||
.validate_bond_tx_hex(&payload.trade_data.signed_bond_hex, &bond_requirements)
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
return Ok(StatusCode::NOT_ACCEPTABLE.into_response());
|
||||
}
|
||||
}
|
||||
debug!("\n\nReceived taker bond: {:?}", payload);
|
||||
|
||||
match handle_taker_bond(&payload, coordinator).await {
|
||||
Ok(offer_taken_response) => Ok(Json(offer_taken_response).into_response()),
|
||||
Err(BondError::BondNotFound) => {
|
||||
info!("Bond requirements not found in database");
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
}
|
||||
Err(BondError::InvalidBond(e)) => {
|
||||
warn!("Invalid bond submission: {e}");
|
||||
return Ok(StatusCode::NOT_ACCEPTABLE.into_response());
|
||||
}
|
||||
Err(BondError::CoordinatorError(e)) => {
|
||||
error!("Coordinator error on bond submission: {e}");
|
||||
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
|
||||
}
|
||||
Err(_) => return Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
}
|
||||
debug!("\nTaker bond validation successful");
|
||||
|
||||
panic!("Trade contract PSBT not implemented!");
|
||||
let trade_contract_psbt_taker = "".to_string(); // implement psbt
|
||||
let trade_contract_psbt_maker = "".to_string(); // implement psbt
|
||||
let escrow_tx_txid: String = "".to_string(); // implement txid of psbt
|
||||
|
||||
database
|
||||
.add_taker_info_and_move_table(
|
||||
&payload,
|
||||
&trade_contract_psbt_maker,
|
||||
&trade_contract_psbt_taker,
|
||||
escrow_tx_txid,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(OfferTakenResponse {
|
||||
trade_psbt_hex_to_sign: trade_contract_psbt_taker,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
/// gets polled by the maker and returns the escrow psbt in case the offer has been taken
|
||||
async fn request_offer_status_maker(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
Json(payload): Json<OfferTakenRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let offer = database
|
||||
.fetch_taken_offer_maker(&payload.offer_id_hex, &payload.robohash_hex)
|
||||
.await?;
|
||||
match offer {
|
||||
Some(offer) => Ok(Json(OfferTakenResponse {
|
||||
trade_psbt_hex_to_sign: offer,
|
||||
})
|
||||
.into_response()),
|
||||
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
debug!("\n\nReceived offer status request: {:?}", payload);
|
||||
|
||||
match get_offer_status_maker(&payload, coordinator).await {
|
||||
Ok(offer_taken_response) => Ok(Json(offer_taken_response).into_response()),
|
||||
Err(FetchOffersError::NoOffersAvailable) => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
Err(FetchOffersError::DatabaseError(e)) => {
|
||||
error!("Database error fetching offers: {e}");
|
||||
Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,8 +119,7 @@ async fn request_offer_status_maker(
|
||||
/// coordinator then has to check if their signatures are valid and everything else is according to the agreed upon contract.
|
||||
/// Once the coordinator has received both partitial signed PSBTs he can assemble them together to a transaction and publish it to the bitcoin network.
|
||||
async fn submit_escrow_psbt(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(wallet): Extension<Arc<CoordinatorWallet<sled::Tree>>>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
Json(payload): Json<PsbtSubmissionRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
panic!("implement")
|
||||
@ -192,22 +134,20 @@ async fn submit_escrow_psbt(
|
||||
/// then the traders will know it is secure to begin with the fiat exchange and can continue with the trade (exchange information in the chat and transfer fiat).
|
||||
/// In theory this polling mechanism could also be replaced by the traders scanning the blockchain themself so they could also see once the tx is confirmed.
|
||||
async fn poll_escrow_confirmation(
|
||||
Extension(database): Extension<Arc<CoordinatorDB>>,
|
||||
Extension(coordinator): Extension<Arc<Coordinator>>,
|
||||
Json(payload): Json<OfferTakenRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
if !database
|
||||
.is_valid_robohash_in_table(&payload.robohash_hex, &payload.offer_id_hex)
|
||||
.await?
|
||||
{
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
}
|
||||
if database
|
||||
.fetch_escrow_tx_confirmation_status(&payload.offer_id_hex)
|
||||
.await?
|
||||
{
|
||||
return Ok(StatusCode::OK.into_response());
|
||||
} else {
|
||||
return Ok(StatusCode::ACCEPTED.into_response());
|
||||
match fetch_escrow_confirmation_status(&payload, coordinator).await {
|
||||
Ok(true) => Ok(StatusCode::OK.into_response()),
|
||||
Ok(false) => Ok(StatusCode::ACCEPTED.into_response()),
|
||||
Err(FetchEscrowConfirmationError::NotFoundError) => {
|
||||
info!("Escrow confirmation check transaction not found");
|
||||
Ok(StatusCode::NOT_FOUND.into_response())
|
||||
}
|
||||
Err(FetchEscrowConfirmationError::DatabaseError(e)) => {
|
||||
error!("Database error fetching escrow confirmation: {e}");
|
||||
Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,9 +255,6 @@ async fn test_api() -> &'static str {
|
||||
}
|
||||
|
||||
pub async fn api_server(coordinator: Arc<Coordinator>) -> Result<()> {
|
||||
let database = Arc::clone(&coordinator.coordinator_db);
|
||||
let wallet = Arc::clone(&coordinator.coordinator_wallet);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/test", get(test_api))
|
||||
.route("/create-offer", post(receive_order))
|
||||
@ -333,8 +270,7 @@ pub async fn api_server(coordinator: Arc<Coordinator>) -> Result<()> {
|
||||
)
|
||||
.route("/request-escrow", post(request_escrow))
|
||||
.route("/poll-final-payout", post(poll_final_payout))
|
||||
.layer(Extension(database))
|
||||
.layer(Extension(wallet));
|
||||
.layer(Extension(coordinator));
|
||||
// add other routes here
|
||||
|
||||
let port: u16 = env::var("PORT")
|
||||
@ -351,7 +287,8 @@ pub async fn api_server(coordinator: Arc<Coordinator>) -> Result<()> {
|
||||
// ANYHOW ERROR HANDLING
|
||||
// --------------
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
struct AppError(anyhow::Error);
|
||||
#[derive(Debug)]
|
||||
pub struct AppError(anyhow::Error);
|
||||
|
||||
// Tell axum how to convert `AppError` into a response.
|
||||
impl IntoResponse for AppError {
|
||||
|
@ -1,43 +1,18 @@
|
||||
use anyhow::Context;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl OrderRequest {
|
||||
pub fn sanity_check(&self) -> Result<()> {
|
||||
// Get the current time
|
||||
let now = SystemTime::now();
|
||||
// Convert the current time to a UNIX timestamp
|
||||
let unix_timestamp = now
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("Time went backwards")?
|
||||
.as_secs();
|
||||
if self.amount_satoshi < 10000 {
|
||||
return Err(anyhow!("Amount too low"));
|
||||
}
|
||||
if self.amount_satoshi > 20000000 {
|
||||
return Err(anyhow!("Amount too high"));
|
||||
}
|
||||
if self.bond_ratio < 2 || self.bond_ratio > 50 {
|
||||
return Err(anyhow!("Bond ratio out of bounds"));
|
||||
}
|
||||
if self.offer_duration_ts < unix_timestamp + 10800 {
|
||||
return Err(anyhow!("Offer duration too short"));
|
||||
}
|
||||
if self.offer_duration_ts > unix_timestamp + 604800 {
|
||||
return Err(anyhow!("Offer duration too long"));
|
||||
}
|
||||
Ok(())
|
||||
pub fn validate_timestamp(offer_duration_ts: u64) -> Result<(), ValidationError> {
|
||||
// Get the current time
|
||||
let now = SystemTime::now();
|
||||
// Convert the current time to a UNIX timestamp
|
||||
let unix_timestamp = now
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
if offer_duration_ts < unix_timestamp + 10800 {
|
||||
return Err(ValidationError::new("Offer duration too short"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_random_order_id(len: usize) -> String {
|
||||
// Generate `len` random bytes
|
||||
let bytes: Vec<u8> = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Standard)
|
||||
.take(len)
|
||||
.collect();
|
||||
|
||||
// Convert bytes to hex string
|
||||
let hex_string = hex::encode(bytes);
|
||||
hex_string
|
||||
if offer_duration_ts > unix_timestamp + 604800 {
|
||||
return Err(ValidationError::new("Offer duration too long"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,227 @@
|
||||
pub mod create_taproot;
|
||||
// pub mod mempool_actor;
|
||||
pub mod mempool_monitoring;
|
||||
pub mod monitoring;
|
||||
pub mod tx_confirmation_monitoring; // commented out for testing
|
||||
pub mod tx_confirmation_monitoring;
|
||||
pub mod utils;
|
||||
|
||||
use self::utils::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BondError {
|
||||
InvalidBond(String),
|
||||
BondNotFound,
|
||||
CoordinatorError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FetchOffersError {
|
||||
NoOffersAvailable,
|
||||
DatabaseError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FetchEscrowConfirmationError {
|
||||
NotFoundError,
|
||||
DatabaseError(String),
|
||||
}
|
||||
|
||||
pub async fn process_order(
|
||||
coordinator: Arc<Coordinator>,
|
||||
offer: &OfferRequest,
|
||||
) -> Result<BondRequirementResponse, AppError> {
|
||||
let wallet = &coordinator.coordinator_wallet;
|
||||
let database = &coordinator.coordinator_db;
|
||||
|
||||
let bond_address = wallet.get_new_address().await?;
|
||||
let locking_amount_sat = offer.amount_satoshi * offer.bond_ratio as u64 / 100;
|
||||
|
||||
let bond_requirements = BondRequirementResponse {
|
||||
bond_address,
|
||||
locking_amount_sat,
|
||||
};
|
||||
|
||||
database
|
||||
.insert_new_maker_request(offer, &bond_requirements)
|
||||
.await?;
|
||||
|
||||
debug!("Coordinator received new offer: {:?}", offer);
|
||||
Ok(bond_requirements)
|
||||
}
|
||||
|
||||
pub async fn handle_maker_bond(
|
||||
payload: &BondSubmissionRequest,
|
||||
coordinator: Arc<Coordinator>,
|
||||
) -> Result<OfferActivatedResponse, BondError> {
|
||||
let wallet = &coordinator.coordinator_wallet;
|
||||
let database = &coordinator.coordinator_db;
|
||||
|
||||
let bond_requirements = if let Ok(requirements) = database
|
||||
.fetch_bond_requirements(&payload.robohash_hex)
|
||||
.await
|
||||
{
|
||||
requirements
|
||||
} else {
|
||||
return Err(BondError::BondNotFound);
|
||||
};
|
||||
|
||||
match wallet
|
||||
.validate_bond_tx_hex(&payload.signed_bond_hex, &bond_requirements)
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
return Err(BondError::InvalidBond(e.to_string()));
|
||||
}
|
||||
}
|
||||
debug!("\nBond validation successful");
|
||||
let offer_id_hex: String = 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 = match wallet.get_new_address().await {
|
||||
Ok(address) => address,
|
||||
Err(e) => {
|
||||
let error = format!(
|
||||
"Error generating taker bond address for offer id: {}. Error: {e}",
|
||||
offer_id_hex
|
||||
);
|
||||
return Err(BondError::CoordinatorError(error.to_string()));
|
||||
}
|
||||
};
|
||||
// insert bond into sql database and move offer to different table
|
||||
let bond_locked_until_timestamp = match database
|
||||
.move_offer_to_active(&payload, &offer_id_hex, new_taker_bond_address)
|
||||
.await
|
||||
{
|
||||
Ok(timestamp) => timestamp,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Error in validate_bond_tx_hex in move_offer_to_active: {}",
|
||||
e
|
||||
);
|
||||
return Err(BondError::CoordinatorError(e.to_string()));
|
||||
}
|
||||
};
|
||||
Ok(OfferActivatedResponse {
|
||||
bond_locked_until_timestamp,
|
||||
offer_id_hex,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_public_offers(
|
||||
request: &OffersRequest,
|
||||
coordinator: Arc<Coordinator>,
|
||||
) -> Result<PublicOffers, FetchOffersError> {
|
||||
let database = &coordinator.coordinator_db;
|
||||
|
||||
let offers = match database.fetch_suitable_offers(request).await {
|
||||
Ok(offers) => offers,
|
||||
Err(e) => {
|
||||
return Err(FetchOffersError::DatabaseError(e.to_string()));
|
||||
}
|
||||
};
|
||||
if offers.is_none() {
|
||||
return Err(FetchOffersError::NoOffersAvailable);
|
||||
}
|
||||
Ok(PublicOffers { offers })
|
||||
}
|
||||
|
||||
pub async fn handle_taker_bond(
|
||||
payload: &OfferPsbtRequest,
|
||||
coordinator: Arc<Coordinator>,
|
||||
) -> Result<OfferTakenResponse, BondError> {
|
||||
let wallet = &coordinator.coordinator_wallet;
|
||||
let database = &coordinator.coordinator_db;
|
||||
|
||||
let bond_requirements = database
|
||||
.fetch_taker_bond_requirements(&payload.offer.offer_id_hex)
|
||||
.await;
|
||||
|
||||
match bond_requirements {
|
||||
Ok(bond_requirements) => {
|
||||
match wallet
|
||||
.validate_bond_tx_hex(&payload.trade_data.signed_bond_hex, &bond_requirements)
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
return Err(BondError::InvalidBond(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(BondError::BondNotFound),
|
||||
}
|
||||
debug!("\nTaker bond validation successful");
|
||||
|
||||
panic!("Trade contract PSBT not implemented!");
|
||||
let trade_contract_psbt_taker = "".to_string(); // implement psbt
|
||||
let trade_contract_psbt_maker = "".to_string(); // implement psbt
|
||||
let escrow_tx_txid: String = "".to_string(); // implement txid of psbt
|
||||
|
||||
if let Err(e) = database
|
||||
.add_taker_info_and_move_table(
|
||||
&payload,
|
||||
&trade_contract_psbt_maker,
|
||||
&trade_contract_psbt_taker,
|
||||
escrow_tx_txid,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(BondError::CoordinatorError(e.to_string()));
|
||||
}
|
||||
|
||||
Ok(OfferTakenResponse {
|
||||
trade_psbt_hex_to_sign: trade_contract_psbt_taker,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_offer_status_maker(
|
||||
payload: &OfferTakenRequest,
|
||||
coordinator: Arc<Coordinator>,
|
||||
) -> Result<OfferTakenResponse, FetchOffersError> {
|
||||
let database = &coordinator.coordinator_db;
|
||||
|
||||
let offer = match database
|
||||
.fetch_taken_offer_maker(&payload.offer_id_hex, &payload.robohash_hex)
|
||||
.await
|
||||
{
|
||||
Ok(offer) => offer,
|
||||
Err(e) => {
|
||||
return Err(FetchOffersError::DatabaseError(e.to_string()));
|
||||
}
|
||||
};
|
||||
match offer {
|
||||
Some(offer) => Ok(OfferTakenResponse {
|
||||
trade_psbt_hex_to_sign: offer,
|
||||
}),
|
||||
None => Err(FetchOffersError::NoOffersAvailable),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_escrow_confirmation_status(
|
||||
payload: &OfferTakenRequest,
|
||||
coordinator: Arc<Coordinator>,
|
||||
) -> Result<bool, FetchEscrowConfirmationError> {
|
||||
let database = &coordinator.coordinator_db;
|
||||
|
||||
match database
|
||||
.is_valid_robohash_in_table(&payload.robohash_hex, &payload.offer_id_hex)
|
||||
.await
|
||||
{
|
||||
Ok(false) => return Err(FetchEscrowConfirmationError::NotFoundError),
|
||||
Ok(true) => (),
|
||||
Err(e) => return Err(FetchEscrowConfirmationError::DatabaseError(e.to_string())),
|
||||
}
|
||||
|
||||
if match database
|
||||
.fetch_escrow_tx_confirmation_status(&payload.offer_id_hex)
|
||||
.await
|
||||
{
|
||||
Ok(status) => status,
|
||||
Err(e) => return Err(FetchEscrowConfirmationError::DatabaseError(e.to_string())),
|
||||
} {
|
||||
// rust smh
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(FetchEscrowConfirmationError::NotFoundError)
|
||||
}
|
||||
}
|
||||
|
13
taptrade-cli-demo/coordinator/src/coordinator/utils.rs
Normal file
13
taptrade-cli-demo/coordinator/src/coordinator/utils.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use super::*;
|
||||
|
||||
pub fn generate_random_order_id(len: usize) -> String {
|
||||
// Generate `len` random bytes
|
||||
let bytes: Vec<u8> = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Standard)
|
||||
.take(len)
|
||||
.collect();
|
||||
|
||||
// Convert bytes to hex string
|
||||
let hex_string = hex::encode(bytes);
|
||||
hex_string
|
||||
}
|
@ -29,7 +29,7 @@ async fn test_insert_new_maker_request() -> Result<()> {
|
||||
let database = create_coordinator().await?;
|
||||
|
||||
// Create a sample order request and bond requirement response
|
||||
let order_request = OrderRequest {
|
||||
let order_request = OfferRequest {
|
||||
robohash_hex: "a3f1f1f0e2f3f4f5".to_string(),
|
||||
is_buy_order: true,
|
||||
amount_satoshi: 1000,
|
||||
|
@ -147,7 +147,7 @@ impl CoordinatorDB {
|
||||
|
||||
pub async fn insert_new_maker_request(
|
||||
&self,
|
||||
order: &OrderRequest,
|
||||
order: &OfferRequest,
|
||||
bond_requirements: &BondRequirementResponse,
|
||||
) -> Result<()> {
|
||||
sqlx::query(
|
||||
|
@ -5,16 +5,17 @@ mod wallet;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bdk::sled;
|
||||
use communication::{api::*, api_server};
|
||||
use coordinator::monitoring::monitor_bonds;
|
||||
use coordinator::monitoring::*;
|
||||
use communication::{api::*, api_server, *};
|
||||
use coordinator::tx_confirmation_monitoring::update_transaction_confirmations;
|
||||
use coordinator::{monitoring::*, *};
|
||||
use database::CoordinatorDB;
|
||||
use dotenv::dotenv;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use rand::Rng;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
use validator::{Validate, ValidationError};
|
||||
use wallet::*;
|
||||
|
||||
pub struct Coordinator {
|
||||
|
Reference in New Issue
Block a user