diff --git a/docs/TapTrade_obs/.obsidian/workspace.json b/docs/TapTrade_obs/.obsidian/workspace.json index 84380f5..10fc4ab 100755 --- a/docs/TapTrade_obs/.obsidian/workspace.json +++ b/docs/TapTrade_obs/.obsidian/workspace.json @@ -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", diff --git a/taptrade-cli-demo/trader/src/communication/api.rs b/taptrade-cli-demo/trader/src/communication/api.rs index 9aadbdf..2f61dd2 100644 --- a/taptrade-cli-demo/trader/src/communication/api.rs +++ b/taptrade-cli-demo/trader/src/communication/api.rs @@ -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>, // 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, +} diff --git a/taptrade-cli-demo/trader/src/communication/mod.rs b/taptrade-cli-demo/trader/src/communication/mod.rs index a82d602..c20659c 100644 --- a/taptrade-cli-demo/trader/src/communication/mod.rs +++ b/taptrade-cli-demo/trader/src/communication/mod.rs @@ -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 { } } } - \ No newline at end of file + +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(()) + } +} diff --git a/taptrade-cli-demo/trader/src/communication/taker_requests.rs b/taptrade-cli-demo/trader/src/communication/taker_requests.rs index a6771da..d964cf2 100644 --- a/taptrade-cli-demo/trader/src/communication/taker_requests.rs +++ b/taptrade-cli-demo/trader/src/communication/taker_requests.rs @@ -42,40 +42,73 @@ impl PublicOffers { } } -impl PublicOffer { tbd - // pub fn take(&self, taker_config: &TraderSettings) -> Result { - // let client = reqwest::blocking::Client::new(); - // let res = client - // .post(format!( - // "{}{}", - // taker_config.coordinator_endpoint, "/take-offer" - // )) - // .json(self) - // .send()? - // .json::()?; - // Ok(res) - // } +impl PublicOffer { + pub fn request_bond(&self, taker_config: &TraderSettings) -> Result { + let client = reqwest::blocking::Client::new(); + let res = client + .post(format!( + "{}{}", + taker_config.coordinator_endpoint, "/request-taker-bond" + )) + .json(self) + .send()? + .json::()?; + Ok(res) + } } -impl OfferTakenRequest { // tbd - // pub fn taker_request( - // bond: &Bond, - // mut musig_data: &MuSigData, - // taker_config: &TraderSettings, - // ) -> Result { - // let request = RequestOfferPsbt { - // offer: - // }; +impl OfferPsbtRequest { + pub fn taker_request( + offer: &PublicOffer, + trade_data: BondSubmissionRequest, + taker_config: &TraderSettings, + ) -> Result { + 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::()?; - // Ok(res) - // } + let client = reqwest::blocking::Client::new(); + let res = client + .post(format!( + "{}{}", + taker_config.coordinator_endpoint, "/submit-taker-bond" + )) + .json(&request) + .send()? + .json::()?; + + 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)); + } + } } diff --git a/taptrade-cli-demo/trader/src/trading/maker_utils.rs b/taptrade-cli-demo/trader/src/trading/maker_utils.rs index 0b8d960..31a755d 100644 --- a/taptrade-cli-demo/trader/src/trading/maker_utils.rs +++ b/taptrade-cli-demo/trader/src/trading/maker_utils.rs @@ -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 { 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); diff --git a/taptrade-cli-demo/trader/src/trading/mod.rs b/taptrade-cli-demo/trader/src/trading/mod.rs index af073dd..f96e6a9 100644 --- a/taptrade-cli-demo/trader/src/trading/mod.rs +++ b/taptrade-cli-demo/trader/src/trading/mod.rs @@ -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(()) } diff --git a/taptrade-cli-demo/trader/src/trading/taker_utils.rs b/taptrade-cli-demo/trader/src/trading/taker_utils.rs index 876ece3..ddd9533 100644 --- a/taptrade-cli-demo/trader/src/trading/taker_utils.rs +++ b/taptrade-cli-demo/trader/src/trading/taker_utils.rs @@ -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 { - // 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 { + // 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 { + 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) } } diff --git a/taptrade-cli-demo/trader/src/trading/utils.rs b/taptrade-cli-demo/trader/src/trading/utils.rs index 0c73f6b..3d4ca35 100644 --- a/taptrade-cli-demo/trader/src/trading/utils.rs +++ b/taptrade-cli-demo/trader/src/trading/utils.rs @@ -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, } diff --git a/taptrade-cli-demo/trader/src/wallet/bond.rs b/taptrade-cli-demo/trader/src/wallet/bond.rs index 4952199..bee743c 100644 --- a/taptrade-cli-demo/trader/src/wallet/bond.rs +++ b/taptrade-cli-demo/trader/src/wallet/bond.rs @@ -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 diff --git a/taptrade-cli-demo/trader/src/wallet/mod.rs b/taptrade-cli-demo/trader/src/wallet/mod.rs index 68c1e7b..dfd1186 100644 --- a/taptrade-cli-demo/trader/src/wallet/mod.rs +++ b/taptrade-cli-demo/trader/src/wallet/mod.rs @@ -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) + } } diff --git a/taptrade_api_mockoon.json b/taptrade_api_mockoon.json index b0429b4..42637e3 100644 --- a/taptrade_api_mockoon.json +++ b/taptrade_api_mockoon.json @@ -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,