Merge branch 'research' of github.com:RoboSats/taptrade-core into research

This commit is contained in:
aaravm
2024-06-19 02:10:39 +05:30
11 changed files with 340 additions and 87 deletions

View File

@ -13,11 +13,11 @@
"state": {
"type": "canvas",
"state": {
"file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas",
"file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas",
"viewState": {
"x": 1097.9522804628996,
"y": -459.7668865449728,
"zoom": 0.19999999999999996
"x": 31,
"y": -256.4238119356301,
"zoom": -0.4572735104698888
}
}
}
@ -88,7 +88,7 @@
"state": {
"type": "backlink",
"state": {
"file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas",
"file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
@ -105,7 +105,7 @@
"state": {
"type": "outgoing-link",
"state": {
"file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas",
"file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas",
"linksCollapsed": false,
"unlinkedCollapsed": true
}
@ -128,7 +128,7 @@
"state": {
"type": "outline",
"state": {
"file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas"
"file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas"
}
}
}
@ -151,6 +151,7 @@
},
"active": "bdb9fd88a01a8909",
"lastOpenFiles": [
"Research/Trade Pipelines/new concepts/concept pipeline 1.canvas",
"Research/Trade Pipelines/new concepts/concept locking script 1.canvas",
"Research/Trade Pipelines/new concepts/concept pipeline 1.canvas",
"Research/Bitcoin fundamentals/Knowledge sources.md",
@ -167,6 +168,8 @@
"Research/Bitcoin fundamentals/Spending Taproot UTXOs.md",
"Research/Bitcoin fundamentals/Signature and Flags.canvas",
"Research/Implementation/CLI demonstrator architecture/demonstrator architecture.canvas",
"Research/Implementation/Libraries.md",
"Research/Implementation/BDK.md",
"Research/Implementation/UI ideas.canvas",
"Research/Trade Pipelines/Existing research.md",
"Research/Implementation/CLI demonstrator architecture",

View File

@ -50,6 +50,7 @@ pub struct OfferTakenResponse {
// Taker structures //
// request all fitting offers from the coordinator
#[derive(Debug, Serialize)]
pub struct OffersRequest {
pub buy_offers: bool, // true if looking for buy offers, false if looking for sell offers
@ -57,19 +58,38 @@ pub struct OffersRequest {
pub amount_max_sat: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PublicOffer {
pub amount_sat: u64,
pub offer_id_hex: String,
}
// response of the coordinator, containing all fitting offers to the OffersRequest request
#[derive(Debug, Deserialize)]
pub struct PublicOffers {
pub offers: Option<Vec<PublicOffer>>, // don't include offers var in return json if no offers are available
}
// Offer information of each offer returned by the previous response
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PublicOffer {
pub amount_sat: u64,
pub offer_id_hex: String,
}
// request to receive the escrow psbt to sign for the specified offer to take it
#[derive(Debug, Serialize)]
pub struct RequestOfferPsbt {
pub struct OfferPsbtRequest {
pub offer: PublicOffer,
pub trade_data: BondSubmissionRequest,
}
// submit signed escrow psbt back to coordinator in a Json like this
#[derive(Debug, Serialize)]
pub struct PsbtSubmissionRequest {
pub signed_psbt_hex: String,
pub offer_id_hex: String,
pub robohash_hex: String,
}
// request polled to check if the maker has submitted his escrow transaction
// and the escrow transaction is confirmed once this returns 200 the chat can open
#[derive(Debug, Serialize)]
pub struct IsOfferReadyRequest {
pub robohash_hex: String,
pub offer_id_hex: String,
}

View File

@ -9,7 +9,7 @@ use crate::{
use anyhow::{anyhow, Result};
use api::{
BondRequirementResponse, BondSubmissionRequest, OfferTakenRequest, OfferTakenResponse,
OrderActivatedResponse, OrderRequest,
OrderActivatedResponse, OrderRequest, PsbtSubmissionRequest,
};
use bdk::bitcoin::consensus::encode::serialize_hex;
use bdk::{
@ -17,6 +17,7 @@ use bdk::{
wallet::AddressInfo,
};
use serde::{Deserialize, Serialize};
use std::{thread::sleep, time::Duration};
impl BondRequirementResponse {
fn _format_request(trader_setup: &TraderSettings) -> OrderRequest {
@ -107,7 +108,7 @@ impl 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.order_id_hex.clone(),
order_id_hex: offer.offer_id_hex.clone(),
};
let client = reqwest::blocking::Client::new();
let res = client
@ -126,4 +127,32 @@ impl OfferTakenResponse {
}
}
}
impl PsbtSubmissionRequest {
pub fn submit_escrow_psbt(
psbt: &PartiallySignedTransaction,
offer_id_hex: String,
taker_config: &TraderSettings,
) -> Result<()> {
let request = PsbtSubmissionRequest {
signed_psbt_hex: psbt.serialize_hex(),
offer_id_hex,
robohash_hex: taker_config.robosats_robohash_hex.clone(),
};
let client = reqwest::blocking::Client::new();
let res = client
.post(format!(
"{}{}",
taker_config.coordinator_endpoint, "/submit-escrow-psbt"
))
.json(&request)
.send()?;
if res.status() != 200 {
return Err(anyhow!(
"Submitting escrow psbt failed. Status: {}",
res.status()
));
}
Ok(())
}
}

View File

@ -42,40 +42,73 @@ impl PublicOffers {
}
}
impl PublicOffer { tbd
// pub fn take(&self, taker_config: &TraderSettings) -> Result<BondRequirementResponse> {
// let client = reqwest::blocking::Client::new();
// let res = client
// .post(format!(
// "{}{}",
// taker_config.coordinator_endpoint, "/take-offer"
// ))
// .json(self)
// .send()?
// .json::<BondRequirementResponse>()?;
// Ok(res)
// }
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 OfferTakenRequest { // tbd
// pub fn taker_request(
// bond: &Bond,
// mut musig_data: &MuSigData,
// taker_config: &TraderSettings,
// ) -> Result<PartiallySignedTransaction> {
// let request = RequestOfferPsbt {
// offer:
// };
impl OfferPsbtRequest {
pub fn taker_request(
offer: &PublicOffer,
trade_data: BondSubmissionRequest,
taker_config: &TraderSettings,
) -> Result<PartiallySignedTransaction> {
let request = OfferPsbtRequest {
offer: offer.clone(),
trade_data,
};
// let client = reqwest::blocking::Client::new();
// let res = client
// .post(format!(
// "{}{}",
// taker_config.coordinator_endpoint, "/submit-taker-bond"
// ))
// .json(self)
// .send()?
// .json::<OfferTakenResponse>()?;
// Ok(res)
// }
let client = reqwest::blocking::Client::new();
let res = client
.post(format!(
"{}{}",
taker_config.coordinator_endpoint, "/submit-taker-bond"
))
.json(&request)
.send()?
.json::<OfferTakenResponse>()?;
let psbt_bytes = hex::decode(res.trade_psbt_hex_to_sign)?;
let psbt = PartiallySignedTransaction::deserialize(&psbt_bytes)?;
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));
}
}
}

View File

@ -16,7 +16,7 @@ impl ActiveOffer {
let (bond, mut musig_data, payout_address) =
trading_wallet.trade_onchain_assembly(&offer_conditions, maker_config)?;
let submission_result = BondSubmissionRequest::send(
let submission_result = BondSubmissionRequest::send_maker(
&maker_config.robosats_robohash_hex,
&bond,
&mut musig_data,
@ -24,23 +24,23 @@ impl ActiveOffer {
maker_config,
)?;
Ok(ActiveOffer {
order_id_hex: submission_result.order_id_hex,
bond_locked_until_timestamp: submission_result.bond_locked_until_timestamp,
offer_id_hex: submission_result.order_id_hex,
used_musig_config: musig_data,
used_bond: bond,
expected_payout_address: payout_address,
escrow_psbt: None,
})
}
// polling until offer is taken, in production a more efficient way would make sense
// returns the PSBT of the escrow trade transaction we have to validate, sign and return
pub fn wait_until_taken(
self,
&self,
trader_config: &TraderSettings,
) -> Result<PartiallySignedTransaction> {
loop {
thread::sleep(Duration::from_secs(10));
if let Some(offer_taken_response) = OfferTakenResponse::check(&self, trader_config)? {
if let Some(offer_taken_response) = OfferTakenResponse::check(self, trader_config)? {
let psbt_bytes = hex::decode(offer_taken_response.trade_psbt_hex_to_sign)?;
let psbt = PartiallySignedTransaction::deserialize(&psbt_bytes)?;
return Ok(psbt);

View File

@ -7,7 +7,7 @@ use crate::{
cli::TraderSettings,
communication::api::{
BondRequirementResponse, BondSubmissionRequest, OfferTakenRequest, OfferTakenResponse,
PublicOffer, PublicOffers,
PsbtSubmissionRequest, PublicOffer, PublicOffers,
},
wallet::{
bond::Bond,
@ -28,7 +28,19 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> {
let offer = ActiveOffer::create(&wallet, maker_config)?;
dbg!(&offer);
let trade_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)?;
// submit signed escrow psbt back to coordinator
PsbtSubmissionRequest::submit_escrow_psbt(
&escrow_contract_psbt,
offer.offer_id_hex.clone(),
maker_config,
)?;
// wait for confirmation
Ok(())
}
@ -43,9 +55,12 @@ pub fn run_taker(taker_config: &TraderSettings) -> Result<()> {
available_offers = PublicOffers::fetch(taker_config)?;
}
let selected_offer: &PublicOffer = available_offers.ask_user_to_select()?;
let accepted_offer = ActiveOffer::take(&wallet, taker_config, selected_offer)?;
accepted_offer.wait_on_maker();
// 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()?;
Ok(())
}

View File

@ -1,20 +1,70 @@
use bdk::electrum_client::Request;
use crate::communication::api::{IsOfferReadyRequest, OfferPsbtRequest, PsbtSubmissionRequest};
use super::utils::*;
use super::*;
impl ActiveOffer {
// pub fn take( tbd
// trading_wallet: &TradingWallet,
// taker_config: &TraderSettings,
// offer: &PublicOffer,
// ) -> Result<ActiveOffer> {
// let bond_conditions: BondRequirementResponse = offer.take(taker_config)?;
// let (bond, mut musig_data, payout_address) =
// trading_wallet.trade_onchain_assembly(&bond_conditions, taker_config)?;
// let trading_psbt =
pub fn take(
trading_wallet: &TradingWallet,
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)?;
// 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)?;
// now we submit the signed bond transaction to the coordinator and receive the escrow PSBT we have to sign
// in exchange
let bond_submission_request = BondSubmissionRequest::prepare_bond_request(
&bond,
&payout_address,
&mut musig_data,
taker_config,
)?;
let mut escrow_contract_psbt =
OfferPsbtRequest::taker_request(offer, bond_submission_request, taker_config)?;
// now we have to verify, sign and submit the escrow psbt again
trading_wallet
.validate_taker_psbt(&escrow_contract_psbt)?
.sign_escrow_psbt(&mut escrow_contract_psbt)?;
// submit signed escrow psbt back to coordinator
PsbtSubmissionRequest::submit_escrow_psbt(
&escrow_contract_psbt,
offer.offer_id_hex.clone(),
taker_config,
)?;
Ok(ActiveOffer {
offer_id_hex: offer.offer_id_hex.clone(),
used_musig_config: musig_data,
used_bond: bond,
expected_payout_address: payout_address,
escrow_psbt: Some(escrow_contract_psbt),
})
}
pub fn wait_on_maker(&self) -> Result<()> {
// tbd
Ok(())
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)
}
}

View File

@ -3,9 +3,9 @@ use super::*;
#[derive(Debug)]
pub struct ActiveOffer {
pub order_id_hex: String,
pub bond_locked_until_timestamp: u128,
pub offer_id_hex: String,
pub used_musig_config: MuSigData,
pub used_bond: PartiallySignedTransaction,
pub expected_payout_address: AddressInfo,
pub escrow_psbt: Option<PartiallySignedTransaction>,
}

View File

@ -57,7 +57,3 @@ impl Bond {
Ok(psbt)
}
}
// impl BranchAndBoundCoinSelection
// pub fn new(size_of_change: u64) -> Self
// Create new instance with target size for change output

View File

@ -3,7 +3,7 @@ pub mod musig2;
pub mod wallet_utils;
use crate::{cli::TraderSettings, communication::api::BondRequirementResponse};
use anyhow::Result;
use anyhow::{anyhow, Result};
use bdk::{
bitcoin::{self, bip32::ExtendedPrivKey, psbt::PartiallySignedTransaction, Network},
blockchain::ElectrumBlockchain,
@ -13,7 +13,7 @@ use bdk::{
miniscript::Descriptor,
template::{Bip86, DescriptorTemplate},
wallet::AddressInfo,
KeychainKind, SyncOptions, Wallet,
KeychainKind, SignOptions, SyncOptions, Wallet,
};
use bond::Bond;
use musig2::MuSigData;
@ -63,13 +63,35 @@ impl TradingWallet {
offer_conditions: &BondRequirementResponse,
trader_config: &TraderSettings,
) -> Result<(PartiallySignedTransaction, MuSigData, AddressInfo)> {
let trading_wallet = self.wallet;
let trading_wallet = &self.wallet;
let bond = Bond::assemble(&self.wallet, &offer_conditions, trader_config)?;
let payout_address: AddressInfo =
trading_wallet.get_address(bdk::wallet::AddressIndex::LastUnused)?;
let mut musig_data =
MuSigData::create(&trader_config.wallet_xprv, trading_wallet.secp_ctx())?;
let musig_data = MuSigData::create(&trader_config.wallet_xprv, trading_wallet.secp_ctx())?;
Ok((bond, musig_data, payout_address))
}
// validate that the taker psbt references the correct inputs and amounts
// taker input should be the same as in the previous bond transaction.
// input amount should be the bond amount when buying,
pub fn validate_taker_psbt(&self, psbt: &PartiallySignedTransaction) -> Result<&Self> {
dbg!("IMPLEMENT TAKER PSBT VALIDATION!");
// tbd once the trade psbt is implemented on coordinator side
Ok(self)
}
pub fn sign_escrow_psbt(&self, escrow_psbt: &mut PartiallySignedTransaction) -> Result<&Self> {
let finalized = self.wallet.sign(escrow_psbt, SignOptions::default())?;
if !finalized {
return Err(anyhow!("Signing of taker escrow psbt failed!"));
}
Ok(self)
}
pub fn validate_maker_psbt(&self, psbt: &PartiallySignedTransaction) -> Result<&Self> {
dbg!("IMPLEMENT MAKER PSBT VALIDATION!");
// tbd once the trade psbt is implemented on coordinator side
Ok(self)
}
}

View File

@ -78,7 +78,7 @@
"body": "{\n \"trade_psbt_hex_to_sign\": \"DEADBEEF\",\n}",
"latency": 0,
"statusCode": 200,
"label": "Returned of the offer has been taken",
"label": "Returned if the offer has been taken",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
@ -94,7 +94,7 @@
},
{
"uuid": "9ada9097-97e3-40c3-b0cf-d0eec3587027",
"body": "{}",
"body": "",
"latency": 0,
"statusCode": 204,
"label": "Returned if the requested offer is not yet taken",
@ -126,7 +126,7 @@
"body": " \"offers\": [\n {\n \"amount_sat\": 1000,\n \"offer_id_hex\": \"abc123\"\n },\n {\n \"amount_sat\": 2000,\n \"offer_id_hex\": \"def456\"\n }\n ]",
"latency": 0,
"statusCode": 200,
"label": "",
"label": "Returns a list of available offers, requested with OffersRequest",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
@ -148,14 +148,14 @@
"type": "http",
"documentation": "",
"method": "post",
"endpoint": "take-offer",
"endpoint": "request-taker-bond",
"responses": [
{
"uuid": "e527cfa3-eaf6-4972-882b-b723084dfe49",
"body": "{\n \"bond_address\": \"tb1pfdvgfzwp8vhmelpv8w9kezz7nsmxw68jz6yehgze6mzx0t6r9t2qv9ynmm\",\n \"locking_amount\": 123456\n}",
"latency": 0,
"statusCode": 200,
"label": "",
"label": "Gets requested with PublicOffer Json",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
@ -171,6 +171,83 @@
}
],
"responseMode": null
},
{
"uuid": "cb932396-c73e-4a4c-9f1a-b4de5250cb16",
"type": "http",
"documentation": "gets requested with OfferPsbtRequest",
"method": "post",
"endpoint": "submit-taker-bond",
"responses": [
{
"uuid": "b852f55c-5c97-47d2-a149-cbf6a0fad3e1",
"body": "{\n \"trade_psbt_hex_to_sign\": \"INVALID_EXAMPLE_37346634343237352D373930622D343631342D626139332D65636637323565303638376337346634343237352D373930622D343631342D626139332D65636637323565303638376337346634343237352D373930622D343631342D626139332D65636637323565303638376337346634343237352D373930622D343631342D626139332D65636637323565303638376337346634343237352D373930622D343631342D626139332D656366373235653036383763\",\n}",
"latency": 0,
"statusCode": 200,
"label": "returns OfferTakenResponse if bond is valid",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": true,
"crudKey": "id",
"callbacks": []
}
],
"responseMode": null
},
{
"uuid": "34bda159-89cf-453c-bf5c-fa532b35f202",
"type": "http",
"documentation": "Taker submits the psbt as json to the coordinator. ",
"method": "post",
"endpoint": "submit-escrow-psbt",
"responses": [
{
"uuid": "e9f64b19-94d0-4356-8892-d05466abbc5d",
"body": "",
"latency": 0,
"statusCode": 200,
"label": "Returned if psbt/signature is valid.",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": true,
"crudKey": "id",
"callbacks": []
},
{
"uuid": "fdfb8fc1-1d54-46fc-b5ad-174f761120c1",
"body": "{}",
"latency": 0,
"statusCode": 406,
"label": "Returned if the signature is invalid or the psbt has been changed ",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": false,
"crudKey": "id",
"callbacks": []
}
],
"responseMode": null
}
],
"rootChildren": [
@ -193,6 +270,14 @@
{
"type": "route",
"uuid": "ee4bb798-c787-43ae-9a34-64568ba8c31b"
},
{
"type": "route",
"uuid": "cb932396-c73e-4a4c-9f1a-b4de5250cb16"
},
{
"type": "route",
"uuid": "34bda159-89cf-453c-bf5c-fa532b35f202"
}
],
"proxyMode": false,