mirror of
https://github.com/RoboSats/taptrade-core.git
synced 2026-02-08 19:40:28 +00:00
merging
This commit is contained in:
1
taptrade-cli-demo/coordinator/.env
Normal file
1
taptrade-cli-demo/coordinator/.env
Normal file
@ -0,0 +1 @@
|
||||
ELECTRUM_BACKEND=127.0.0.1:50001
|
||||
1161
taptrade-cli-demo/coordinator/Cargo.lock
generated
1161
taptrade-cli-demo/coordinator/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,12 +4,18 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
axum = "0.7.5"
|
||||
bdk = "0.29.0"
|
||||
clap = { version = "4.5.4", features = ["derive", "cargo"] }
|
||||
frost-secp256k1 = "1.0.0"
|
||||
dotenv = "0.15.0"
|
||||
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
|
||||
serde = "1.0.203"
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] }
|
||||
tokio = "1.38.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 3
|
||||
strip = true
|
||||
anyhow = "1.0.86"
|
||||
hex = "0.4"
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
use clap::{command, Arg, Command, ArgMatches};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Coordinator;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TraderSettings {
|
||||
pub electrum_endpoint: String,
|
||||
pub coordinator_endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CliSettings {
|
||||
Coordinator(Coordinator),
|
||||
Taker(TraderSettings),
|
||||
Maker(TraderSettings)
|
||||
}
|
||||
|
||||
trait ArgMatchesParser {
|
||||
fn parse_into_enum(&self) -> CliSettings;
|
||||
}
|
||||
|
||||
impl ArgMatchesParser for ArgMatches {
|
||||
fn parse_into_enum(&self) -> CliSettings {
|
||||
if let Some(_mode) = self.subcommand_matches("coordinator") {
|
||||
CliSettings::Coordinator(Coordinator { })
|
||||
} else if let Some(mode) = self.subcommand_matches("trader") {
|
||||
let trader_settings = TraderSettings {
|
||||
coordinator_endpoint: mode.get_one::<String>("coordinator-ep")
|
||||
.expect("Coordinator endpoint not provided!").clone(),
|
||||
electrum_endpoint: mode.get_one::<String>("electrum-ep")
|
||||
.expect("Electrum endpoint not provided").clone()
|
||||
};
|
||||
if mode.contains_id("maker") {
|
||||
CliSettings::Maker( trader_settings )
|
||||
} else if mode.contains_id("taker") {
|
||||
CliSettings::Taker( trader_settings )
|
||||
} else {
|
||||
panic!("Wrong arguments for Trader mode!")
|
||||
}
|
||||
} else {
|
||||
panic!("Select either coordinator or trader mode!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_cli_args() -> CliSettings {
|
||||
command!()
|
||||
.about("RoboSats taproot onchain trade pipeline CLI demonstrator. Don't use with real funds.")
|
||||
.subcommand(
|
||||
Command::new("coordinator")
|
||||
.about("Run in coordinator mode.")
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("trader")
|
||||
.about("Two available trader modes: Maker and Taker. Select one and provide Coordinator and Electum endpoint")
|
||||
.arg(
|
||||
Arg::new("taker")
|
||||
.short('t')
|
||||
.long("taker")
|
||||
.help("Run program as taker")
|
||||
.num_args(0)
|
||||
.conflicts_with("maker")
|
||||
)
|
||||
.arg (
|
||||
Arg::new("maker")
|
||||
.short('m')
|
||||
.long("maker")
|
||||
.num_args(0)
|
||||
.help("Run program as maker")
|
||||
.conflicts_with("taker")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("coordinator-ep")
|
||||
.short('p')
|
||||
.long("endpoint")
|
||||
.required(true)
|
||||
.help("Communication endpoint of the coordinator to connect to")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("electrum-ep")
|
||||
.short('e')
|
||||
.long("electrum")
|
||||
.required(true)
|
||||
.help("URL of the electrum endpoint")
|
||||
)
|
||||
)
|
||||
.arg_required_else_help(true)
|
||||
.get_matches()
|
||||
.parse_into_enum()
|
||||
}
|
||||
@ -1,83 +1,97 @@
|
||||
pub mod api;
|
||||
mod api;
|
||||
|
||||
use verify_bond::verify_psbt;
|
||||
use reqwest::StatusCode;
|
||||
use axum::{routing::post, Json, Router, response::{IntoResponse, Response}, };
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use api::{
|
||||
BondRequirementResponse, BondSubmissionRequest, OrderActivatedResponse, OrderRequest,
|
||||
};
|
||||
|
||||
// use super::*;
|
||||
// use api::{BondRequirementResponse, BondSubmissionRequest, OrderActivatedResponse, OrderRequest};
|
||||
// use axum::{
|
||||
// http::StatusCode, response::IntoResponse, response::Response, routing::post, Extension, Json,
|
||||
// Router,
|
||||
// };
|
||||
// use sqlx::sqlite::SqliteLockingMode;
|
||||
use crate::verify_bond::verify_psbt;
|
||||
|
||||
// Handler function to process the received data
|
||||
async fn receive_order(Json(order): Json<OrderRequest>)-> Json<BondRequirementResponse> {
|
||||
// Print the received data to the console
|
||||
println!("Received order: {:?}", order);
|
||||
async fn receive_order(
|
||||
Extension(state): Extension<Arc<Coordinator>>,
|
||||
Json(order): Json<OrderRequest>,
|
||||
) -> Result<Json<BondRequirementResponse>, AppError> {
|
||||
// Connecting to SQLite database
|
||||
let db_pool = state.db_pool.clone();
|
||||
let mut conn = db_pool.acquire().await.unwrap();
|
||||
|
||||
// Access individual fields
|
||||
// let robohash = &order.robohash_hex;
|
||||
let amount = order.amount_satoshi;
|
||||
// let order_type = &order.is_buy_order;
|
||||
let bond_ratio = order.bond_ratio;
|
||||
// let offer_duration= order.offer_duration_ts;
|
||||
// sqlx::query!(
|
||||
// "INSERT INTO orders (field1, field2) VALUES (?, ?)",
|
||||
// order.field1,
|
||||
// order.field2
|
||||
// )
|
||||
|
||||
// Create a response struct
|
||||
let response = BondRequirementResponse {
|
||||
bond_address: "Order received successfully".to_string(),
|
||||
// Add any other fields you want to include in your response
|
||||
locking_amount_sat: (amount * bond_ratio as u64 / 100),
|
||||
};
|
||||
|
||||
// Return the response as JSON
|
||||
Json(response)
|
||||
// insert offer into sql database
|
||||
// generate locking address for bond
|
||||
|
||||
println!("Coordinator received new offer: {:?}", order);
|
||||
Ok(Json(BondRequirementResponse {
|
||||
bond_address: bond_address,
|
||||
locking_amount_sat: order.amount_satoshi * order.bond_ratio as u64 / 100,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn submit_maker_bond(
|
||||
Json(payload): Json<BondSubmissionRequest>,
|
||||
) -> Json<OrderActivatedResponse> {
|
||||
// Process the payload
|
||||
// For now, we'll just return a dummy success response
|
||||
let response = OrderActivatedResponse {
|
||||
bond_locked_until_timestamp: 0 as u128,
|
||||
order_id_hex: "Bond submitted successfully".to_string(),
|
||||
};
|
||||
// async fn submit_maker_bond(
|
||||
// Json(payload): Json<BondSubmissionRequest>,
|
||||
// ) -> Result<Json<OrderActivatedResponse>, AppError> {
|
||||
// // Process the payload
|
||||
// // For now, we'll just return a dummy success response
|
||||
// let response = OrderActivatedResponse {
|
||||
// bond_locked_until_timestamp: 0 as u128,
|
||||
// order_id_hex: "Bond submitted successfully".to_string(),
|
||||
// };
|
||||
|
||||
// Create the JSON response
|
||||
Json(response)
|
||||
}
|
||||
// // Create the JSON response
|
||||
// Json(response)
|
||||
// }
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn webserver() {
|
||||
// Build our application with a single route
|
||||
pub async fn api_server(coordinator: Coordinator) -> Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/create-offer", post(receive_order))
|
||||
.route("/submit-maker-bond", post(submit_maker_bond));
|
||||
// .route("/submit-maker-bond", post(submit_maker_bond));
|
||||
.layer(Extension(coordinator));
|
||||
// add other routes here
|
||||
|
||||
// Run the server on localhost:3000
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
println!("Listening on {}", addr);
|
||||
// axum::Server::bind(&addr)
|
||||
// .serve(app.into_make_service())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// Run the server on localhost:9999
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 9999));
|
||||
let tcp = TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(tcp, app).await.unwrap();
|
||||
axum::serve(tcp, app).await?;
|
||||
println!("Listening on {}", addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // use axum
|
||||
// ANYHOW ERROR HANDLING
|
||||
// --------------
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
struct AppError(anyhow::Error);
|
||||
|
||||
// #[get("/")]
|
||||
// fn index() -> &'static str {
|
||||
// "Hello, world!"
|
||||
// }
|
||||
// Tell axum how to convert `AppError` into a response.
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", self.0),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// #[launch]
|
||||
// pub fn webserver() -> Rocket<build> {
|
||||
// rocket::build().mount("/", routes![index])
|
||||
// }
|
||||
|
||||
// // serde to parse json
|
||||
// // https://www.youtube.com/watch?v=md-ecvXBGzI BDK + Webserver video
|
||||
// // https://github.com/tokio-rs/axum
|
||||
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
|
||||
// `Result<_, AppError>`. That way you don't need to do that manually.
|
||||
impl<E> From<E> for AppError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,38 @@
|
||||
mod coordinator;
|
||||
mod cli;
|
||||
mod communication;
|
||||
mod coordinator;
|
||||
|
||||
use cli::parse_cli_args;
|
||||
use communication::webserver;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::Wallet;
|
||||
use communication::api_server;
|
||||
use dotenv::dotenv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() {
|
||||
webserver();
|
||||
let mode = parse_cli_args();
|
||||
dbg!(mode);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Coordinator {
|
||||
pub db_pool: Arc<Pool<Sqlite>>,
|
||||
pub wallet: Arc<Wallet<MemoryDatabase>>, // using sqlite for Wallet?
|
||||
}
|
||||
|
||||
// test with cargo run -- trader --maker --endpoint "taptrade-coordinator.com:5432" --electrum "electrum-server.com:50002"
|
||||
// populate .env with values before starting
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenv().ok();
|
||||
// Initialize the database pool
|
||||
let db_pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:./db/trades.db")
|
||||
.await
|
||||
.unwrap();
|
||||
let shared_db_pool: Arc<sqlx::Pool<sqlx::Sqlite>> = Arc::new(db_pool);
|
||||
|
||||
let coordinator = Coordinator {
|
||||
db_pool: shared_db_pool,
|
||||
wallet: // impl wallet
|
||||
};
|
||||
|
||||
api_server(coordinator).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -15,3 +15,8 @@ rand_core = "0.6.4"
|
||||
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
|
||||
serde = "1.0.203"
|
||||
sha2 = "0.10.8"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
strip = true
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use crate::wallet::get_wallet_xprv;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bdk::bitcoin::bip32::ExtendedPrivKey;
|
||||
use hex;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
env,
|
||||
@ -90,7 +89,7 @@ impl CliSettings {
|
||||
}
|
||||
|
||||
// parses the hours input string and returns the unix timestamp + the trade duration in seconds
|
||||
fn hours_to_ts(hours: &String) -> Result<u64> {
|
||||
fn hours_to_ts(hours: &str) -> Result<u64> {
|
||||
let duration: u64 = hours.parse()?;
|
||||
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + duration * 3600)
|
||||
}
|
||||
@ -98,7 +97,7 @@ impl CliSettings {
|
||||
fn get_trader_settings() -> Result<TraderSettings> {
|
||||
let electrum_endpoint = Self::get_user_input("Enter electrum endpoint: ");
|
||||
let coordinator_endpoint = Self::get_user_input("Enter coordinator endpoint: ");
|
||||
let robosats_robohash_hex = hex::encode(&hash256(&Self::get_user_input(
|
||||
let robosats_robohash_hex = hex::encode(hash256(&Self::get_user_input(
|
||||
"Enter your robosats robot key: ", // just for testing purposes, to be improved to the real robohash spec
|
||||
)));
|
||||
let trade_type: OfferType = Self::get_trade_type(None);
|
||||
|
||||
@ -22,7 +22,7 @@ pub struct BondRequirementResponse {
|
||||
// maker step 2
|
||||
// (submission of signed bond and other data neccessary to coordinate the trade)
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct BondSubmissionRequest {
|
||||
pub struct BondSubmissionRequest {
|
||||
pub robohash_hex: String,
|
||||
pub signed_bond_hex: String, // signed bond transaction, hex encoded
|
||||
pub payout_address: String, // does this make sense here?
|
||||
@ -93,3 +93,15 @@ pub struct IsOfferReadyRequest {
|
||||
pub robohash_hex: String,
|
||||
pub offer_id_hex: String,
|
||||
}
|
||||
|
||||
// request posted by both parties when the trade obligations
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TradeObligationsSatisfied {
|
||||
pub robohash_hex: String,
|
||||
pub offer_id_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PayoutPsbtResponse {
|
||||
pub payout_psbt_hex: String,
|
||||
}
|
||||
|
||||
@ -8,8 +8,9 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use api::{
|
||||
BondRequirementResponse, BondSubmissionRequest, OfferTakenRequest, OfferTakenResponse,
|
||||
OrderActivatedResponse, OrderRequest, PsbtSubmissionRequest,
|
||||
BondRequirementResponse, BondSubmissionRequest, IsOfferReadyRequest, OfferTakenRequest,
|
||||
OfferTakenResponse, OrderActivatedResponse, OrderRequest, PayoutPsbtResponse,
|
||||
PsbtSubmissionRequest, TradeObligationsSatisfied,
|
||||
};
|
||||
use bdk::bitcoin::consensus::encode::serialize_hex;
|
||||
use bdk::{
|
||||
@ -17,7 +18,7 @@ use bdk::{
|
||||
wallet::AddressInfo,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{thread::sleep, time::Duration};
|
||||
use std::{str::FromStr, thread::sleep, time::Duration};
|
||||
|
||||
impl BondRequirementResponse {
|
||||
fn _format_request(trader_setup: &TraderSettings) -> OrderRequest {
|
||||
@ -78,7 +79,7 @@ impl BondSubmissionRequest {
|
||||
}
|
||||
|
||||
pub fn send_maker(
|
||||
robohash_hex: &String,
|
||||
robohash_hex: &str,
|
||||
bond: &PartiallySignedTransaction,
|
||||
musig_data: &mut MuSigData,
|
||||
payout_address: &AddressInfo,
|
||||
@ -106,7 +107,6 @@ impl OfferTakenResponse {
|
||||
trader_setup: &TraderSettings,
|
||||
) -> Result<Option<OfferTakenResponse>> {
|
||||
let request = OfferTakenRequest {
|
||||
// maybe can be made a bit more efficient (less clone)
|
||||
robohash_hex: trader_setup.robosats_robohash_hex.clone(),
|
||||
order_id_hex: offer.offer_id_hex.clone(),
|
||||
};
|
||||
@ -156,3 +156,105 @@ impl PsbtSubmissionRequest {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TradeObligationsSatisfied {
|
||||
// if the trader is satisfied he can submit this to signal the coordinator readiness to close the trade
|
||||
// if the other party also submits this the coordinator can initiate the closing transaction, otherwise
|
||||
// escrow has to be initiated
|
||||
pub fn submit(offer_id_hex: &String, trader_config: &TraderSettings) -> Result<()> {
|
||||
let request = TradeObligationsSatisfied {
|
||||
robohash_hex: trader_config.robosats_robohash_hex.clone(),
|
||||
offer_id_hex: offer_id_hex.clone(),
|
||||
};
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}{}",
|
||||
trader_config.coordinator_endpoint, "/submit-obligation-confirmation"
|
||||
))
|
||||
.json(&request)
|
||||
.send()?;
|
||||
if res.status() != 200 {
|
||||
return Err(anyhow!(
|
||||
"Submitting trade obligations confirmation failed. Status: {}",
|
||||
res.status()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IsOfferReadyRequest {
|
||||
pub fn poll(taker_config: &TraderSettings, offer: &ActiveOffer) -> Result<()> {
|
||||
let request = IsOfferReadyRequest {
|
||||
robohash_hex: taker_config.robosats_robohash_hex.clone(),
|
||||
offer_id_hex: offer.offer_id_hex.clone(),
|
||||
};
|
||||
let client = reqwest::blocking::Client::new();
|
||||
loop {
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}{}",
|
||||
taker_config.coordinator_endpoint, "/poll-offer-status"
|
||||
))
|
||||
.json(&request)
|
||||
.send()?;
|
||||
if res.status() == 200 {
|
||||
return Ok(());
|
||||
} else if res.status() != 204 {
|
||||
return Err(anyhow!(
|
||||
"Requesting offer status when waiting on other party failed: {}",
|
||||
res.status()
|
||||
));
|
||||
}
|
||||
// Sleep for 10 sec and poll again
|
||||
sleep(Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_payout(
|
||||
trader_config: &TraderSettings,
|
||||
offer: &ActiveOffer,
|
||||
) -> Result<Option<PartiallySignedTransaction>> {
|
||||
let request = IsOfferReadyRequest {
|
||||
robohash_hex: trader_config.robosats_robohash_hex.clone(),
|
||||
offer_id_hex: offer.offer_id_hex.clone(),
|
||||
};
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let mut res: reqwest::blocking::Response;
|
||||
|
||||
loop {
|
||||
// Sleep for 10 sec and poll
|
||||
sleep(Duration::from_secs(10));
|
||||
|
||||
res = client
|
||||
.post(format!(
|
||||
"{}{}",
|
||||
trader_config.coordinator_endpoint, "/poll-final-payout"
|
||||
))
|
||||
.json(&request)
|
||||
.send()?;
|
||||
if res.status() == 200 {
|
||||
// good case, psbt is returned
|
||||
break;
|
||||
} else if res.status() == 204 {
|
||||
// still waiting, retry
|
||||
continue;
|
||||
} else if res.status() == 201 {
|
||||
// other party initiated escrow
|
||||
return Ok(None);
|
||||
} else {
|
||||
// unintended response
|
||||
return Err(anyhow!(
|
||||
"Requesting final payout when waiting on other party failed: {}",
|
||||
res.status()
|
||||
));
|
||||
}
|
||||
}
|
||||
let final_psbt = PartiallySignedTransaction::from_str(
|
||||
&res.json::<PayoutPsbtResponse>()?.payout_psbt_hex,
|
||||
)?;
|
||||
Ok(Some(final_psbt))
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,32 +83,3 @@ impl OfferPsbtRequest {
|
||||
Ok(psbt)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsOfferReadyRequest {
|
||||
pub fn poll(taker_config: &TraderSettings, offer: &ActiveOffer) -> Result<()> {
|
||||
let request = IsOfferReadyRequest {
|
||||
robohash_hex: taker_config.robosats_robohash_hex.clone(),
|
||||
offer_id_hex: offer.offer_id_hex.clone(),
|
||||
};
|
||||
let client = reqwest::blocking::Client::new();
|
||||
loop {
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}{}",
|
||||
taker_config.coordinator_endpoint, "/poll-offer-status-taker"
|
||||
))
|
||||
.json(&request)
|
||||
.send()?;
|
||||
if res.status() == 200 {
|
||||
return Ok(());
|
||||
} else if res.status() != 201 {
|
||||
return Err(anyhow!(
|
||||
"Submitting taker psbt failed. Status: {}",
|
||||
res.status()
|
||||
));
|
||||
}
|
||||
// Sleep for 10 sec and poll again
|
||||
sleep(Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,9 @@ use self::utils::ActiveOffer;
|
||||
use crate::{
|
||||
cli::TraderSettings,
|
||||
communication::api::{
|
||||
BondRequirementResponse, BondSubmissionRequest, OfferTakenRequest, OfferTakenResponse,
|
||||
PsbtSubmissionRequest, PublicOffer, PublicOffers,
|
||||
BondRequirementResponse, BondSubmissionRequest, IsOfferReadyRequest, OfferTakenRequest,
|
||||
OfferTakenResponse, PsbtSubmissionRequest, PublicOffer, PublicOffers,
|
||||
TradeObligationsSatisfied,
|
||||
},
|
||||
wallet::{
|
||||
bond::Bond,
|
||||
@ -28,8 +29,8 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> {
|
||||
|
||||
let offer = ActiveOffer::create(&wallet, maker_config)?;
|
||||
dbg!(&offer);
|
||||
let mut escrow_contract_psbt = offer.wait_until_taken(maker_config)?;
|
||||
|
||||
let mut escrow_contract_psbt = offer.wait_until_taken(maker_config)?;
|
||||
wallet
|
||||
.validate_maker_psbt(&escrow_contract_psbt)?
|
||||
.sign_escrow_psbt(&mut escrow_contract_psbt)?;
|
||||
@ -40,8 +41,18 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> {
|
||||
offer.offer_id_hex.clone(),
|
||||
maker_config,
|
||||
)?;
|
||||
// wait for confirmation
|
||||
|
||||
// wait for confirmation
|
||||
offer.wait_on_trade_ready_confirmation(maker_config)?;
|
||||
if offer.fiat_confirmation_cli_input(maker_config)? {
|
||||
// this represents the "confirm payment" / "confirm fiat recieved" button
|
||||
TradeObligationsSatisfied::submit(&offer.offer_id_hex, maker_config)?;
|
||||
println!("Waiting for other party to confirm the trade.");
|
||||
let payout_keyspend_psbt = IsOfferReadyRequest::poll_payout(maker_config, &offer)?;
|
||||
} else {
|
||||
println!("Trade failed.");
|
||||
panic!("Escrow to be implemented!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -49,18 +60,27 @@ pub fn run_taker(taker_config: &TraderSettings) -> Result<()> {
|
||||
let wallet = TradingWallet::load_wallet(taker_config)?;
|
||||
let mut available_offers = PublicOffers::fetch(taker_config)?;
|
||||
|
||||
while let None = available_offers.offers {
|
||||
println!("No offers available, trying again in 10 sec.");
|
||||
while available_offers.offers.is_none() {
|
||||
println!("No offers available, fetching again in 10 sec.");
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
available_offers = PublicOffers::fetch(taker_config)?;
|
||||
}
|
||||
let selected_offer: &PublicOffer = available_offers.ask_user_to_select()?;
|
||||
|
||||
// take selected offer and wait for maker to sign his input to the ecrow transaction
|
||||
let accepted_offer =
|
||||
ActiveOffer::take(&wallet, taker_config, selected_offer)?.wait_on_maker(taker_config)?;
|
||||
|
||||
accepted_offer.wait_on_fiat_confirmation()?;
|
||||
let accepted_offer = ActiveOffer::take(&wallet, taker_config, selected_offer)?;
|
||||
accepted_offer.wait_on_trade_ready_confirmation(taker_config)?;
|
||||
|
||||
if accepted_offer.fiat_confirmation_cli_input(taker_config)? {
|
||||
// this represents the "confirm payment" / "confirm fiat recieved" button
|
||||
TradeObligationsSatisfied::submit(&accepted_offer.offer_id_hex, taker_config)?;
|
||||
println!("Waiting for other party to confirm the trade.");
|
||||
// pull for other parties confirmation, then receive the transaction to create MuSig signature for (keyspend) to payout address
|
||||
let payout_keyspend_psbt = IsOfferReadyRequest::poll_payout(taker_config, &accepted_offer)?;
|
||||
// here we need to handle if the other party is not cooperating
|
||||
} else {
|
||||
println!("Trade failed.");
|
||||
panic!("Escrow to be implemented!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -49,22 +49,4 @@ impl ActiveOffer {
|
||||
escrow_psbt: Some(escrow_contract_psbt),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn wait_on_maker(self, taker_config: &TraderSettings) -> Result<Self> {
|
||||
IsOfferReadyRequest::poll(taker_config, &self)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn wait_on_fiat_confirmation(&self) -> Result<&Self> {
|
||||
// let user confirm in CLI that the fiat payment has been sent/receivec
|
||||
loop {
|
||||
println!("Please confirm that the fiat payment has been sent/received. (y/N)");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if input.trim().to_lowercase() == "y" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use super::maker_utils::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -9,3 +8,34 @@ pub struct ActiveOffer {
|
||||
pub expected_payout_address: AddressInfo,
|
||||
pub escrow_psbt: Option<PartiallySignedTransaction>,
|
||||
}
|
||||
|
||||
impl ActiveOffer {
|
||||
// polls till the other party signed the trade transaction and it got confirmed.
|
||||
// once the coordinator signals OfferReady the fiat exchange can begin
|
||||
pub fn wait_on_trade_ready_confirmation(
|
||||
&self,
|
||||
trader_config: &TraderSettings,
|
||||
) -> Result<&Self> {
|
||||
IsOfferReadyRequest::poll(trader_config, &self)?;
|
||||
Ok(&self)
|
||||
}
|
||||
|
||||
pub fn fiat_confirmation_cli_input(&self, trade_settings: &TraderSettings) -> Result<bool> {
|
||||
// let user confirm in CLI that the fiat payment has been sent/received
|
||||
println!("The escrow is now locked and the fiat exchange can begin safely.");
|
||||
if trade_settings.trade_type.is_buy_order() {
|
||||
println!("Please confirm that the fiat payment has been sent or go into mediation in case of problems. (y/M)");
|
||||
} else {
|
||||
println!("Please confirm that the fiat payment has been received or go into mediation in case of problems. (y/M)");
|
||||
}
|
||||
loop {
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if input.trim().to_lowercase() == "y" {
|
||||
return Ok(true);
|
||||
} else if input.trim() == "M" {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,11 +43,8 @@ impl TradingWallet {
|
||||
pub fn load_wallet(trader_config: &TraderSettings) -> Result<TradingWallet> {
|
||||
let backend = ElectrumBlockchain::from(Client::new(&trader_config.electrum_endpoint)?);
|
||||
let wallet = Wallet::new(
|
||||
Bip86(trader_config.wallet_xprv.clone(), KeychainKind::External),
|
||||
Some(Bip86(
|
||||
trader_config.wallet_xprv.clone(),
|
||||
KeychainKind::Internal,
|
||||
)),
|
||||
Bip86(trader_config.wallet_xprv, KeychainKind::External),
|
||||
Some(Bip86(trader_config.wallet_xprv, KeychainKind::Internal)),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(), // non-permanent storage
|
||||
)?;
|
||||
@ -64,7 +61,7 @@ impl TradingWallet {
|
||||
trader_config: &TraderSettings,
|
||||
) -> Result<(PartiallySignedTransaction, MuSigData, AddressInfo)> {
|
||||
let trading_wallet = &self.wallet;
|
||||
let bond = Bond::assemble(&self.wallet, &offer_conditions, trader_config)?;
|
||||
let bond = Bond::assemble(&self.wallet, offer_conditions, trader_config)?;
|
||||
let payout_address: AddressInfo =
|
||||
trading_wallet.get_address(bdk::wallet::AddressIndex::LastUnused)?;
|
||||
let musig_data = MuSigData::create(&trader_config.wallet_xprv, trading_wallet.secp_ctx())?;
|
||||
|
||||
@ -1 +1,2 @@
|
||||
tbd
|
||||
Thinks to improve when implementing the production ready library:
|
||||
* make api more generic (smaller)
|
||||
|
||||
Reference in New Issue
Block a user