From 04c6957ccfc21efaee4ed6eb2de80544741d673a Mon Sep 17 00:00:00 2001 From: Armando Dutra Date: Thu, 7 Dec 2023 01:46:09 -0300 Subject: [PATCH] refactor: add strategy swaps --- src/bin/bitmaskd.rs | 70 +++- src/bitcoin/payment.rs | 6 +- src/bitcoin/psbt.rs | 8 +- src/carbonado.rs | 104 +++++- src/carbonado/error.rs | 2 + src/constants.rs | 5 +- src/rgb.rs | 654 +++++++++++++++++++++++++-------- src/rgb/auction.rs | 0 src/rgb/cambria.rs | 4 +- src/rgb/carbonado.rs | 126 ++++++- src/rgb/crdt.rs | 19 +- src/rgb/fs.rs | 35 +- src/rgb/swap.rs | 418 +++++++++++++++++---- src/structs.rs | 74 +++- tests/rgb/integration/swaps.rs | 16 +- tests/rgb/web/swaps.rs | 4 +- 16 files changed, 1250 insertions(+), 295 deletions(-) create mode 100644 src/rgb/auction.rs diff --git a/src/bin/bitmaskd.rs b/src/bin/bitmaskd.rs index 75b8af78..1e1be0e2 100644 --- a/src/bin/bitmaskd.rs +++ b/src/bin/bitmaskd.rs @@ -25,9 +25,9 @@ use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, SecretKey}; use bitmask_core::{ bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, carbonado::{ - handle_file, + handle_file, marketplace_retrieve, marketplace_store, metrics::{metrics, metrics_csv}, - server_retrieve, server_store, store, + store, }, constants::{ get_marketplace_nostr_key, get_marketplace_seed, get_network, get_udas_utxo, switch_network, @@ -524,7 +524,7 @@ async fn co_server_store( body: Bytes, ) -> Result { info!("POST /carbonado/server/{name}, {} bytes", body.len()); - let (filepath, encoded) = server_store(&name, &body, None).await?; + let (filepath, encoded) = marketplace_store(&name, &body, None).await?; match OpenOptions::new() .read(true) @@ -624,7 +624,7 @@ async fn co_metadata( async fn co_server_retrieve(Path(name): Path) -> Result { info!("GET /server/{name}"); - let result = server_retrieve(&name).await; + let result = marketplace_retrieve(&name).await; let cc = CacheControl::new().with_no_cache(); match result { @@ -684,6 +684,56 @@ async fn rgb_proxy_media_data_save( Ok((StatusCode::OK, Json(resp))) } +async fn rgb_auction_get_offer( + Path(offer_id): Path, + Json(_request): Json, +) -> Result { + info!("GET /auction/{offer_id}"); + Ok((StatusCode::OK, Json(""))) +} + +async fn rgb_auction_create_offer( + TypedHeader(_auth): TypedHeader>, + Path(offer_id): Path, + Json(_request): Json, +) -> Result { + info!("POST /auction/{offer_id}"); + Ok((StatusCode::OK, Json(""))) +} + +async fn rgb_auction_destroy_offer( + TypedHeader(_auth): TypedHeader>, + Path(offer_id): Path, +) -> Result { + info!("DELETE /auction/{offer_id}"); + Ok((StatusCode::OK, Json(""))) +} + +async fn rgb_auction_get_bid( + Path((offer_id, bid_id)): Path<(String, String)>, + Json(_request): Json, +) -> Result { + info!("GET /auction/{offer_id}/{bid_id}"); + Ok((StatusCode::OK, Json(""))) +} + +async fn rgb_auction_create_bid( + TypedHeader(_auth): TypedHeader>, + Path((offer_id, bid_id)): Path<(String, String)>, + Json(_request): Json, +) -> Result { + info!("POST /auction/{offer_id}/{bid_id}"); + Ok((StatusCode::OK, Json(""))) +} + +async fn rgb_auction_destroy_bid( + TypedHeader(_auth): TypedHeader>, + Path((offer_id, bid_id)): Path<(String, String)>, +) -> Result { + info!("DELETE /auction/{offer_id}/{bid_id}"); + Ok((StatusCode::OK, Json(""))) +} + const BMC_VERSION: &str = env!("CARGO_PKG_VERSION"); async fn status() -> Result { @@ -822,6 +872,18 @@ async fn main() -> Result<()> { .route("/proxy/media-metadata", post(rgb_proxy_media_data_save)) .route("/proxy/media-metadata/:id", get(rgb_proxy_media_retrieve)) .route("/proxy/media/:id", get(rgb_proxy_metadata_retrieve)) + .route("/auctions/:offer_id", get(rgb_auction_get_offer)) + .route("/auctions/:offer_id", post(rgb_auction_create_offer)) + .route("/auctions/:offer_id", delete(rgb_auction_destroy_offer)) + .route("/auctions/:offer_id/bid/:bid_id", get(rgb_auction_get_bid)) + .route( + "/auctions/:offer_id/bid/:bid_id", + post(rgb_auction_create_bid), + ) + .route( + "/auction/:offer_id/bid/:bid_id", + delete(rgb_auction_destroy_bid), + ) .route("/metrics.json", get(json_metrics)) .route("/metrics.csv", get(csv_metrics)); diff --git a/src/bitcoin/payment.rs b/src/bitcoin/payment.rs index ea8ac4d4..7e46dcec 100644 --- a/src/bitcoin/payment.rs +++ b/src/bitcoin/payment.rs @@ -93,9 +93,9 @@ pub async fn create_payjoin( .into_iter() .enumerate() .find(|(_, txo)| { - invoices.iter().all(|invoice| { - txo.script_pubkey != invoice.address.script_pubkey() - }) + invoices + .iter() + .all(|invoice| txo.script_pubkey != invoice.address.script_pubkey()) }) .map(|(i, _)| i); diff --git a/src/bitcoin/psbt.rs b/src/bitcoin/psbt.rs index 263bc122..0f177c7f 100644 --- a/src/bitcoin/psbt.rs +++ b/src/bitcoin/psbt.rs @@ -9,6 +9,9 @@ use crate::{ #[derive(Error, Debug)] pub enum BitcoinPsbtError { + /// Could not broadcast PSBT + #[error("Could not broadcast PSBT")] + CouldNotBroadcastPsbt(String), /// Could not finalize when signing PSBT #[error("Could not finalize when signing PSBT")] CouldNotFinalizePsbt, @@ -70,7 +73,10 @@ pub async fn publish_psbt( let tx = psbt.extract_tx(); debug!("tx:", &serialize(&tx.clone()).to_hex()); let blockchain = get_blockchain().await; - blockchain.broadcast(&tx).await?; + blockchain + .broadcast(&tx) + .await + .map_err(|op| BitcoinPsbtError::CouldNotBroadcastPsbt(op.to_string()))?; let txid = tx.txid(); let tx = blockchain.get_tx(&txid).await?; diff --git a/src/carbonado.rs b/src/carbonado.rs index d2f02444..759df4a1 100644 --- a/src/carbonado.rs +++ b/src/carbonado.rs @@ -9,19 +9,24 @@ pub mod error; pub mod metrics; #[cfg(not(target_arch = "wasm32"))] -pub use server::{handle_file, retrieve, retrieve_metadata, server_retrieve, server_store, store}; +pub use server::{ + auctions_retrieve, auctions_store, handle_file, marketplace_retrieve, marketplace_store, + retrieve, retrieve_metadata, store, +}; #[cfg(not(target_arch = "wasm32"))] mod server { - use crate::constants::get_marketplace_nostr_key; + use crate::constants::{get_coordinator_nostr_key, get_marketplace_nostr_key}; use super::*; use std::{ io::{Error, ErrorKind}, path::PathBuf, + str::FromStr, }; + use bitcoin_30::secp256k1::ecdh::SharedSecret; use tokio::fs; pub async fn store( @@ -51,7 +56,7 @@ mod server { Ok(()) } - pub async fn server_store( + pub async fn marketplace_store( name: &str, input: &[u8], metadata: Option>, @@ -78,6 +83,37 @@ mod server { Ok((filepath, body)) } + pub async fn auctions_store( + bundle_id: &str, + name: &str, + input: &[u8], + metadata: Option>, + ) -> Result<(PathBuf, Vec), CarbonadoError> { + let coordinator_key: String = get_coordinator_nostr_key().await; + + let level = 15; + let sk = hex::decode(coordinator_key)?; + let secret_key = SecretKey::from_slice(&sk)?; + let public_key = + PublicKey::from_str(bundle_id).map_err(|_| CarbonadoError::WrongNostrPublicKey)?; + + let share_sk = SharedSecret::new(&public_key, &secret_key); + let pk = share_sk.secret_bytes(); + let pk_hex = hex::encode(pk); + + let mut meta: Option<[u8; 8]> = default!(); + if let Some(metadata) = metadata { + let mut inner: [u8; 8] = default!(); + inner[..metadata.len()].copy_from_slice(&metadata); + meta = Some(inner); + } + + let (body, _encode_info) = carbonado::file::encode(&sk, Some(&pk), input, level, meta)?; + let filepath = handle_file(&pk_hex, name, body.len()).await?; + fs::write(filepath.clone(), body.clone()).await?; + Ok((filepath, body)) + } + pub async fn retrieve( sk: &str, name: &str, @@ -120,7 +156,9 @@ mod server { Ok((Vec::new(), None)) } - pub async fn server_retrieve(name: &str) -> Result<(Vec, Option>), CarbonadoError> { + pub async fn marketplace_retrieve( + name: &str, + ) -> Result<(Vec, Option>), CarbonadoError> { let marketplace_key: String = get_marketplace_nostr_key().await; let sk = hex::decode(marketplace_key)?; @@ -144,6 +182,37 @@ mod server { Ok((Vec::new(), None)) } + pub async fn auctions_retrieve( + bundle_id: &str, + name: &str, + ) -> Result<(Vec, Option>), CarbonadoError> { + let coordinator_key: String = get_coordinator_nostr_key().await; + + let sk = hex::decode(coordinator_key)?; + let secret_key = SecretKey::from_slice(&sk)?; + let public_key = + PublicKey::from_str(bundle_id).map_err(|_| CarbonadoError::WrongNostrPublicKey)?; + + let share_sk = SharedSecret::new(&public_key, &secret_key); + let pk = share_sk.secret_bytes(); + let pk = hex::encode(pk); + + let mut final_name = name.to_string(); + let network = NETWORK.read().await.to_string(); + let networks = ["bitcoin", "testnet", "signet", "regtest"]; + if !networks.into_iter().any(|x| name.contains(x)) { + final_name = format!("{network}-{name}"); + } + + let filepath = handle_file(&pk, &final_name, 0).await?; + if let Ok(bytes) = fs::read(filepath).await { + let (header, decoded) = carbonado::file::decode(&sk, &bytes)?; + return Ok((decoded, header.metadata.map(|m| m.to_vec()))); + } + + Ok((Vec::new(), None)) + } + pub async fn handle_file( pk: &str, name: &str, @@ -210,7 +279,10 @@ mod server { } #[cfg(target_arch = "wasm32")] -pub use client::{retrieve, retrieve_metadata, server_retrieve, server_store, store}; +pub use client::{ + auctions_retrieve, auctions_store, marketplace_retrieve, marketplace_store, retrieve, + retrieve_metadata, store, +}; #[cfg(target_arch = "wasm32")] mod client { @@ -296,7 +368,7 @@ mod client { } } - pub async fn server_store( + pub async fn marketplace_store( name: &str, input: &[u8], _metadata: Option>, @@ -328,6 +400,15 @@ mod client { } } + pub async fn auctions_store( + _bundle_id: &str, + _name: &str, + _input: &[u8], + _metadata: Option>, + ) -> Result<(), CarbonadoError> { + todo!() + } + pub async fn retrieve_metadata(sk: &str, name: &str) -> Result { let sk = hex::decode(sk)?; let secret_key = SecretKey::from_slice(&sk)?; @@ -416,7 +497,9 @@ mod client { Ok((Vec::new(), None)) } - pub async fn server_retrieve(name: &str) -> Result<(Vec, Option>), CarbonadoError> { + pub async fn marketplace_retrieve( + name: &str, + ) -> Result<(Vec, Option>), CarbonadoError> { let network = NETWORK.read().await.to_string(); let endpoints = CARBONADO_ENDPOINT.read().await.to_string(); let endpoints: Vec<&str> = endpoints.split(',').collect(); @@ -438,6 +521,13 @@ mod client { Ok((encoded.to_vec(), None)) } + pub async fn auctions_retrieve( + _bundle_id: &str, + _name: &str, + ) -> Result<(Vec, Option>), CarbonadoError> { + todo!() + } + async fn fetch_post(url: String, body: Arc>) -> Result { let array = Uint8Array::new_with_length(body.len() as u32); array.copy_from(&body); diff --git a/src/carbonado/error.rs b/src/carbonado/error.rs index aa3bb9a6..7e893a7d 100644 --- a/src/carbonado/error.rs +++ b/src/carbonado/error.rs @@ -27,6 +27,8 @@ pub enum CarbonadoError { AllEndpointsFailed, /// Wrong Nostr private key WrongNostrPrivateKey, + /// Wrong Nostr public key + WrongNostrPublicKey, /// Debug: {0} Debug(String), } diff --git a/src/constants.rs b/src/constants.rs index e838f4ec..3df62cbf 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -76,6 +76,10 @@ pub async fn get_marketplace_fee_xpub() -> String { MARKETPLACE_FEE_XPUB.read().await.to_string() } +pub async fn get_coordinator_nostr_key() -> String { + MARKETPLACE_NOSTR.read().await.to_string() +} + pub static UDAS_UTXO: Lazy> = Lazy::new(|| RwLock::new(dot_env("UDAS_UTXO"))); pub async fn get_udas_utxo() -> String { @@ -220,5 +224,4 @@ pub mod storage_keys { pub const ASSETS_OFFERS: &str = "bitmask-asset_offers.c15"; pub const ASSETS_BIDS: &str = "bitmask-asset_bids.c15"; pub const MARKETPLACE_OFFERS: &str = "bitmask-marketplace_public_offers.c15"; - pub const MARKETPLACE_BIDS: &str = "bitmask-marketplace_public_bids.c15"; } diff --git a/src/rgb.rs b/src/rgb.rs index b4b60ed2..b42849b8 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -19,6 +19,7 @@ use rgbstd::{ }; use rgbwallet::{psbt::DbcPsbtError, RgbInvoice}; use std::{ + cmp::Ordering, collections::{BTreeMap, HashMap, HashSet}, ops::Sub, str::FromStr, @@ -47,6 +48,7 @@ pub mod transfer; pub mod wallet; use crate::{ + bitcoin::{publish_psbt_file, sign_psbt_file}, constants::{get_network, BITCOIN_EXPLORER_API, NETWORK}, rgb::{ issue::{issue_contract as create_contract, IssueContractError}, @@ -65,15 +67,17 @@ use crate::{ IssueMediaRequest, IssueRequest, IssueResponse, MediaEncode, MediaRequest, MediaResponse, MediaView, NextAddressResponse, NextUtxoResponse, NextUtxosResponse, PsbtFeeRequest, PsbtRequest, PsbtResponse, PublicRgbBidResponse, PublicRgbOfferResponse, - PublicRgbOffersResponse, ReIssueRequest, ReIssueResponse, RgbBidDetail, RgbBidRequest, - RgbBidResponse, RgbBidsResponse, RgbInternalSaveTransferRequest, - RgbInternalTransferResponse, RgbInvoiceResponse, RgbOfferBidsResponse, RgbOfferDetail, - RgbOfferRequest, RgbOfferResponse, RgbOfferUpdateRequest, RgbOfferUpdateResponse, - RgbOffersResponse, RgbRemoveTransferRequest, RgbReplaceResponse, RgbSaveTransferRequest, - RgbSwapRequest, RgbSwapResponse, RgbTransferDetail, RgbTransferRequest, + PublicRgbOffersResponse, PublishPsbtRequest, ReIssueRequest, ReIssueResponse, + RgbAuctionBidRequest, RgbAuctionBidResponse, RgbBidDetail, RgbBidRequest, RgbBidResponse, + RgbBidsResponse, RgbInternalSaveTransferRequest, RgbInternalTransferResponse, + RgbInvoiceResponse, RgbOfferBidsResponse, RgbOfferDetail, RgbOfferRequest, + RgbOfferResponse, RgbOfferUpdateRequest, RgbOfferUpdateResponse, RgbOffersResponse, + RgbRemoveTransferRequest, RgbReplaceResponse, RgbSaveTransferRequest, RgbSwapRequest, + RgbSwapResponse, RgbSwapStatusResponse, RgbTransferDetail, RgbTransferRequest, RgbTransferResponse, RgbTransferStatusResponse, RgbTransfersResponse, SchemaDetail, - SchemasResponse, SimpleContractResponse, TransferType, TxStatus, UtxoResponse, - WatcherDetailResponse, WatcherRequest, WatcherResponse, WatcherUtxoResponse, + SchemasResponse, SignPsbtRequest, SignedPsbtResponse, SimpleContractResponse, TransferType, + TxStatus, UtxoResponse, WatcherDetailResponse, WatcherRequest, WatcherResponse, + WatcherUtxoResponse, }, validators::RGBContext, }; @@ -113,10 +117,12 @@ use self::{ RgbTransferV1, RgbTransfersV1, }, swap::{ - get_public_offer, get_swap_bid, get_swap_bid_by_buyer, get_swap_bids_by_seller, - mark_bid_fill, mark_offer_fill, mark_transfer_bid, mark_transfer_offer, publish_public_bid, - publish_public_offer, publish_swap_bid, remove_public_offers, PsbtSwapEx, RgbBid, - RgbBidSwap, RgbOffer, RgbOfferErrors, RgbOfferSwap, RgbOrderStatus, + get_auction_highest_bid, get_public_offer, get_public_offers, get_swap_bid_by_buyer, + get_swap_bid_by_seller, get_swap_bids_by_offer, mark_bid_fill, mark_offer_fill, + mark_transfer_bid, mark_transfer_offer, publish_auction_bid, publish_auction_offers, + publish_public_bid, publish_public_offer, publish_swap_bid, remove_public_offers, + PsbtSwapEx, RgbBid, RgbBidSwap, RgbOffer, RgbOfferErrors, RgbOfferOptions, RgbOfferSwap, + RgbOrderStatus, RgbSwapStrategy, }, transfer::{extract_transfer, AcceptTransferError, NewInvoiceError, NewPaymentError}, wallet::{ @@ -677,10 +683,10 @@ pub async fn full_transfer_asset( .map_err(TransferError::IO)?; let LocalRgbAccount { - doc, mut rgb_account, + version, } = local_rgb_account; - let mut fork_wallet = automerge::AutoCommit::load(&doc) + let mut fork_wallet = automerge::AutoCommit::load(&version) .map_err(|op| TransferError::WrongAutoMerge(op.to_string()))?; let mut rgb_account_changes = RawRgbAccount::from(rgb_account.clone()); @@ -871,7 +877,9 @@ pub enum RgbSwapError { Create(PsbtError), /// Occurs an error in estimate fee step. {0} Estimate(EstimateFeeError), - /// Occurs an error in publish offer step. {0} + /// Occurs an error in auction step. {0} + Auction(RgbOfferErrors), + /// Occurs an error in public offer step. {0} Marketplace(RgbOfferErrors), /// Occurs an error in invoice step. {0} Invoice(InvoiceError), @@ -889,6 +897,8 @@ pub enum RgbSwapError { WrongNetwork(String), /// Bitcoin address cannot be decoded. {0} WrongAddress(String), + /// This operation cannot support the {0} strategy. + WrongStrategy(String), /// Seller PSBT cannot be decoded. {0} WrongPsbtSeller(String), /// Buyer PSBT cannot be decoded. {0} @@ -897,6 +907,8 @@ pub enum RgbSwapError { WrongPsbtSwap(String), /// Swap Consig Cannot be decoded. {0} WrongConsigSwap(String), + /// Final PSBT cannot be finished. {0} + WrongPsbtFinal(String), } pub async fn create_seller_offer( @@ -911,9 +923,81 @@ pub async fn create_seller_offer( return Err(RgbSwapError::Validation(errors)); } - let network = NETWORK.read().await.to_string(); - let network = - Network::from_str(&network).map_err(|op| RgbSwapError::WrongNetwork(op.to_string()))?; + let mut resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..default!() + }; + + let (mut stock, mut rgb_account) = + retrieve_stock_account(sk).await.map_err(RgbSwapError::IO)?; + + let options = RgbOfferOptions::default(); + let new_offer = internal_create_seller_offer( + sk, + request, + options, + &mut rgb_account, + &mut stock, + &mut resolver, + ) + .await?; + + let RgbOffer { + contract_id, + asset_amount, + asset_precision, + seller_address, + bitcoin_price, + seller_psbt, + strategy, + .. + } = new_offer.clone(); + + let contract_amount = ContractAmount::new(asset_amount, asset_precision).to_string(); + let contract_amount = f64::from_str(&contract_amount).expect("Invalid Contract Amount Value"); + let resp = RgbOfferResponse { + offer_id: new_offer.clone().offer_id, + contract_id: contract_id.clone(), + contract_amount, + bitcoin_price, + seller_address: seller_address.to_string(), + seller_psbt: seller_psbt.clone(), + }; + + let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + my_offers = my_offers.save_offer(contract_id, new_offer.clone()); + + store_offers(sk, my_offers) + .await + .map_err(RgbSwapError::IO)?; + + store_stock_account(sk, stock, rgb_account) + .await + .map_err(RgbSwapError::IO)?; + + let public_offer = RgbOfferSwap::from(new_offer); + + match strategy { + RgbSwapStrategy::P2P | RgbSwapStrategy::HotSwap => publish_public_offer(public_offer) + .await + .map_err(RgbSwapError::Marketplace)?, + invalid => return Err(RgbSwapError::WrongStrategy(invalid.to_string())), + } + + Ok(resp) +} + +pub async fn create_auction_offers( + sk: &str, + request: Vec, +) -> Result, RgbSwapError> { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .iter() + .map(|(f, e)| (f.to_string(), e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), @@ -923,6 +1007,89 @@ pub async fn create_seller_offer( let (mut stock, mut rgb_account) = retrieve_stock_account(sk).await.map_err(RgbSwapError::IO)?; + let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + + let mut resp = vec![]; + let mut collection = vec![]; + let options = RgbOfferOptions::with_bundle_id(sk.to_owned()); + for item in request { + let new_offer = internal_create_seller_offer( + sk, + item, + options.clone(), + &mut rgb_account, + &mut stock, + &mut resolver, + ) + .await?; + + let RgbOffer { + contract_id, + asset_amount, + asset_precision, + seller_address, + bitcoin_price, + seller_psbt, + strategy, + .. + } = new_offer.clone(); + + if !strategy.eq(&RgbSwapStrategy::Auction) { + return Err(RgbSwapError::WrongStrategy(strategy.to_string())); + } + + let contract_amount = ContractAmount::new(asset_amount, asset_precision).to_string(); + let contract_amount = + f64::from_str(&contract_amount).expect("Invalid Contract Amount Value"); + + my_offers = my_offers.save_offer(contract_id.clone(), new_offer.clone()); + collection.push(RgbOfferSwap::from(new_offer.clone())); + + resp.push(RgbOfferResponse { + offer_id: new_offer.clone().offer_id, + contract_id: contract_id.clone(), + contract_amount, + bitcoin_price, + seller_address: seller_address.to_string(), + seller_psbt: seller_psbt.clone(), + }); + } + + store_offers(sk, my_offers) + .await + .map_err(RgbSwapError::IO)?; + + store_stock_account(sk, stock, rgb_account) + .await + .map_err(RgbSwapError::IO)?; + + publish_auction_offers(collection) + .await + .map_err(RgbSwapError::Auction)?; + + Ok(resp) +} + +pub async fn internal_create_seller_offer( + sk: &str, + request: RgbOfferRequest, + options: RgbOfferOptions, + rgb_account: &mut RgbAccountV1, + rgb_stock: &mut Stock, + rgb_resolver: &mut ExplorerResolver, +) -> Result { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .iter() + .map(|(f, e)| (f.to_string(), e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } + + let network = NETWORK.read().await.to_string(); + let network = + Network::from_str(&network).map_err(|op| RgbSwapError::WrongNetwork(op.to_string()))?; + let mut rgb_wallet = match rgb_account.wallets.get(RGB_DEFAULT_NAME) { Some(rgb_wallet) => rgb_wallet.to_owned(), _ => return Err(RgbSwapError::NoWatcher), @@ -934,7 +1101,7 @@ pub async fn create_seller_offer( bitcoin_price, iface, expire_at, - presig, + strategy, change_terminal, .. } = request.clone(); @@ -945,10 +1112,10 @@ pub async fn create_seller_offer( let contr_id = ContractId::from_str(&contract_id).unwrap(); let boilerplate = - export_boilerplate(contr_id, &mut stock).map_err(|_| RgbSwapError::NoContract)?; + export_boilerplate(contr_id, rgb_stock).map_err(|_| RgbSwapError::NoContract)?; let (allocations, asset_inputs, bitcoin_inputs, mut bitcoin_changes, change_value) = - prebuild_seller_swap(request, &mut stock, &mut rgb_wallet, &mut resolver).await?; + prebuild_seller_swap(request, rgb_stock, &mut rgb_wallet, rgb_resolver).await?; rgb_account .wallets @@ -966,63 +1133,28 @@ pub async fn create_seller_offer( rbf: true, }; - let options = NewPsbtOptions::set_inflaction(change_value); - let seller_psbt = - internal_create_psbt(psbt_req, &mut rgb_account, &mut resolver, Some(options)) - .await - .map_err(RgbSwapError::Create)?; - + let psbt_options = NewPsbtOptions::set_inflaction(change_value); + let seller_psbt = internal_create_psbt(psbt_req, rgb_account, rgb_resolver, Some(psbt_options)) + .await + .map_err(RgbSwapError::Create)?; + let contract_amount = ContractAmount::from_decimal_str(contract_amount); let new_offer = RgbOffer::new( sk.to_string(), contract_id.clone(), iface.clone(), allocations, + contract_amount.to_value(), boilerplate.precision, seller_address.address, bitcoin_price, seller_psbt.psbt.clone(), - presig, change_terminal, + strategy, expire_at, + options.bundle_id, ); - let contract_amount = ContractAmount::from_decimal_str(contract_amount).to_string(); - let contract_amount = - f64::from_str(&contract_amount).map_err(|_| RgbSwapError::WrongValue(contract_amount))?; - - let resp = RgbOfferResponse { - offer_id: new_offer.clone().offer_id, - contract_id: contract_id.clone(), - contract_amount, - bitcoin_price, - seller_address: seller_address.to_string(), - seller_psbt: seller_psbt.psbt.clone(), - }; - - let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; - if let Some(offers) = my_offers.offers.get(&contract_id) { - let mut current_offers = offers.to_owned(); - current_offers.push(new_offer.clone()); - my_offers.offers.insert(contract_id, current_offers); - } else { - my_offers - .offers - .insert(contract_id, vec![new_offer.clone()]); - } - - store_offers(sk, my_offers) - .await - .map_err(RgbSwapError::IO)?; - - store_stock_account(sk, stock, rgb_account) - .await - .map_err(RgbSwapError::IO)?; - - let public_offer = RgbOfferSwap::from(new_offer); - publish_public_offer(public_offer) - .await - .map_err(RgbSwapError::Marketplace)?; - Ok(resp) + Ok(new_offer) } pub async fn update_seller_offer( @@ -1077,27 +1209,193 @@ pub async fn create_buyer_bid( sk: &str, request: RgbBidRequest, ) -> Result { - if let Err(err) = request.validate(&RGBContext::default()) { - let errors = err - .iter() - .map(|(f, e)| (f.to_string(), e.to_string())) - .collect(); - return Err(RgbSwapError::Validation(errors)); - } + let (mut stock, mut rgb_account) = + retrieve_stock_account(sk).await.map_err(RgbSwapError::IO)?; let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..default!() }; - let (mut stock, mut rgb_account) = - retrieve_stock_account(sk).await.map_err(RgbSwapError::IO)?; + let (new_bid, resp) = + internal_create_buyer_bid(sk, request, &mut rgb_account, &mut stock, &mut resolver).await?; - let mut rgb_wallet = match rgb_account.wallets.get(RGB_DEFAULT_NAME) { - Some(rgb_wallet) => rgb_wallet.to_owned(), - _ => return Err(RgbSwapError::NoWatcher), + let RgbBid { + offer_id, + contract_id, + .. + } = new_bid.clone(); + + let RgbOfferSwap { + expire_at, + public: offer_pub, + strategy, + .. + } = get_public_offer(offer_id) + .await + .map_err(RgbSwapError::Buyer)?; + + let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; + my_bids = my_bids.save_bid(contract_id, new_bid.clone()); + + store_bids(sk, my_bids).await.map_err(RgbSwapError::IO)?; + store_stock_account(sk, stock, rgb_account) + .await + .map_err(RgbSwapError::IO)?; + + match strategy { + RgbSwapStrategy::HotSwap | RgbSwapStrategy::P2P => { + let public_bid = RgbBidSwap::from(new_bid); + publish_swap_bid(sk, &offer_pub, public_bid.clone(), expire_at) + .await + .map_err(RgbSwapError::Marketplace)?; + + publish_public_bid(public_bid) + .await + .map_err(RgbSwapError::Marketplace)?; + } + invalid => return Err(RgbSwapError::WrongStrategy(invalid.to_string())), + }; + + Ok(resp) +} + +pub async fn create_auction_bid( + sk: &str, + request: RgbAuctionBidRequest, +) -> Result { + let (mut stock, mut rgb_account, mut rgb_transfers) = retrieve_stock_account_transfers(sk) + .await + .map_err(RgbSwapError::IO)?; + + let mut resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..default!() + }; + + let buyer_bid_req = RgbBidRequest::from(request.clone()); + let (new_bid, resp) = internal_create_buyer_bid( + sk, + buyer_bid_req, + &mut rgb_account, + &mut stock, + &mut resolver, + ) + .await?; + + let RgbBid { + iface, contract_id, .. + } = new_bid.clone(); + + let RgbBidResponse { + bid_id, + offer_id, + invoice: buyer_invoice, + swap_psbt, + fee_value, + } = resp.clone(); + + let RgbOfferSwap { strategy, .. } = get_public_offer(offer_id.clone()) + .await + .map_err(RgbSwapError::Buyer)?; + + let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; + my_bids = my_bids.save_bid(contract_id, new_bid.clone()); + + store_bids(sk, my_bids).await.map_err(RgbSwapError::IO)?; + + match strategy { + RgbSwapStrategy::Auction => { + let change_terminal = match iface.to_uppercase().as_str() { + "RGB20" => "/20/1", + "RGB21" => "/21/1", + _ => "/10/1", + }; + + let transfer_req = RgbTransferRequest { + psbt: swap_psbt.clone(), + rgb_invoice: buyer_invoice.to_string(), + terminal: change_terminal.to_string(), + }; + + let params = NewTransferOptions { + offer_id: Some(offer_id.clone()), + bid_id: Some(bid_id.clone()), + ..default!() + }; + + let RgbInternalTransferResponse { + consig_id, + psbt: final_psbt, + consig: final_consig, + outpoint, + commit, + amount, + .. + } = internal_transfer_asset( + transfer_req, + params, + &mut stock, + &mut rgb_account, + &mut rgb_transfers, + ) + .await + .map_err(RgbSwapError::Transfer)?; + + let request = SignPsbtRequest { + psbt: final_psbt, + descriptors: request.sign_keys, + }; + + let SignedPsbtResponse { + psbt: final_psbt, .. + } = sign_psbt_file(request) + .await + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + + let mut bid_swap = RgbBidSwap::from(new_bid); + bid_swap.transfer_id = Some(consig_id.clone()); + bid_swap.transfer = Some(final_consig.clone()); + bid_swap.swap_psbt = Some(final_psbt.clone()); + bid_swap.tap_outpoint = Some(outpoint); + bid_swap.tap_amount = Some(amount); + bid_swap.tap_commit = Some(commit); + + publish_auction_bid(bid_swap) + .await + .map_err(RgbSwapError::Auction)?; + } + invalid => return Err(RgbSwapError::WrongStrategy(invalid.to_string())), + }; + + store_stock_account_transfers(sk, stock, rgb_account, rgb_transfers) + .await + .map_err(RgbSwapError::IO)?; + + let resp = RgbAuctionBidResponse { + bid_id, + offer_id, + fee_value, }; + Ok(resp) +} + +async fn internal_create_buyer_bid( + sk: &str, + request: RgbBidRequest, + rgb_account: &mut RgbAccountV1, + rgb_stock: &mut Stock, + resolver: &mut ExplorerResolver, +) -> Result<(RgbBid, RgbBidResponse), RgbSwapError> { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .iter() + .map(|(f, e)| (f.to_string(), e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } + let RgbBidRequest { offer_id, change_terminal, @@ -1107,17 +1405,36 @@ pub async fn create_buyer_bid( let RgbOfferSwap { iface, seller_psbt, - public: offer_pub, + bitcoin_price, expire_at, .. } = get_public_offer(offer_id) .await .map_err(RgbSwapError::Buyer)?; + let mut rgb_wallet = match rgb_account.wallets.get(RGB_DEFAULT_NAME) { + Some(rgb_wallet) => rgb_wallet.to_owned(), + _ => return Err(RgbSwapError::NoWatcher), + }; + let (mut new_bid, bitcoin_inputs, bitcoin_changes, fee_value) = - prebuild_buyer_swap(sk, request, &mut rgb_wallet, &mut resolver).await?; + prebuild_buyer_swap(sk, request, &mut rgb_wallet, resolver).await?; new_bid.iface = iface.to_uppercase(); + if let Some(expire_at) = expire_at { + let utc = chrono::Local::now().naive_utc().timestamp(); + if expire_at.sub(utc) <= 0 { + return Err(RgbSwapError::OfferExpired); + } + } + + if new_bid.bitcoin_amount.cmp(&bitcoin_price) == Ordering::Less { + return Err(RgbSwapError::Inflation { + input: new_bid.bitcoin_amount, + output: bitcoin_price, + }); + }; + let buyer_outpoint = watcher_next_utxo(sk, RGB_DEFAULT_NAME, &iface.to_uppercase()) .await .map_err(|op| RgbSwapError::NoUtxo(op.to_string()))?; @@ -1147,24 +1464,13 @@ pub async fn create_buyer_bid( ..default!() }; - let buyer_psbt = internal_create_psbt(psbt_req, &mut rgb_account, &mut resolver, Some(options)) + let buyer_psbt = internal_create_psbt(psbt_req, rgb_account, resolver, Some(options)) .await .map_err(RgbSwapError::Create)?; new_bid.buyer_psbt = buyer_psbt.psbt.clone(); let contract_id = &new_bid.contract_id; - let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; - if let Some(bids) = my_bids.bids.get(contract_id) { - let mut current_bids = bids.to_owned(); - current_bids.push(new_bid.clone()); - my_bids.bids.insert(contract_id.clone(), current_bids); - } else { - my_bids - .bids - .insert(contract_id.clone(), vec![new_bid.clone()]); - } - let seller_psbt = Psbt::from_str(&seller_psbt).map_err(|op| RgbSwapError::WrongPsbtSeller(op.to_string()))?; @@ -1189,14 +1495,6 @@ pub async fn create_buyer_bid( .. } = new_bid.clone(); - if let Some(expire_at) = expire_at { - let utc = chrono::Local::now().naive_utc().timestamp(); - - if expire_at.sub(utc) <= 0 { - return Err(RgbSwapError::OfferExpired); - } - } - let invoice_amount = ContractAmount::new(asset_amount, asset_precision); let invoice_req = InvoiceRequest { iface, @@ -1205,7 +1503,7 @@ pub async fn create_buyer_bid( seal: format!("tapret1st:{buyer_outpoint}"), params: HashMap::new(), }; - let invoice = internal_create_invoice(invoice_req, &mut stock) + let invoice = internal_create_invoice(invoice_req, rgb_stock) .await .map_err(RgbSwapError::Invoice)?; @@ -1220,22 +1518,7 @@ pub async fn create_buyer_bid( fee_value, }; - store_bids(sk, my_bids).await.map_err(RgbSwapError::IO)?; - - store_stock_account(sk, stock, rgb_account) - .await - .map_err(RgbSwapError::IO)?; - - let public_bid = RgbBidSwap::from(new_bid); - publish_swap_bid(sk, &offer_pub, public_bid.clone(), expire_at) - .await - .map_err(RgbSwapError::Marketplace)?; - - publish_public_bid(public_bid) - .await - .map_err(RgbSwapError::Marketplace)?; - - Ok(resp) + Ok((new_bid, resp)) } pub async fn create_swap_transfer( @@ -1264,27 +1547,28 @@ pub async fn create_swap_transfer( let RgbOfferSwap { iface, expire_at, - presig, public: offer_pub, + strategy, .. } = get_public_offer(offer_id.clone()) .await .map_err(RgbSwapError::Swap)?; - let mut rgb_swap_bid = if presig { - get_swap_bid_by_buyer(sk, offer_id.clone(), bid_id.clone()) - .await - .map_err(RgbSwapError::Swap)? - } else { - get_swap_bid(sk, offer_id.clone(), bid_id.clone(), expire_at) + let mut rgb_swap_bid = match strategy { + RgbSwapStrategy::P2P | RgbSwapStrategy::Auction => { + get_swap_bid_by_buyer(sk, offer_id.clone(), bid_id.clone()) + .await + .map_err(RgbSwapError::Swap)? + } + _ => get_swap_bid_by_seller(sk, offer_id.clone(), bid_id.clone(), expire_at) .await - .map_err(RgbSwapError::Swap)? + .map_err(RgbSwapError::Swap)?, }; let RgbBidSwap { contract_id, buyer_invoice, - public: bid_pub, + public, .. } = rgb_swap_bid.clone(); let change_terminal = match iface.to_uppercase().as_str() { @@ -1323,47 +1607,52 @@ pub async fn create_swap_transfer( .await .map_err(RgbSwapError::Transfer)?; - if presig { - let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; - mark_transfer_bid(bid_id.clone(), consig_id.clone(), &mut my_bids) - .await - .map_err(RgbSwapError::Swap)?; + let counter_party = match strategy { + RgbSwapStrategy::P2P => { + let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; + mark_transfer_bid(bid_id.clone(), consig_id.clone(), &mut my_bids) + .await + .map_err(RgbSwapError::Swap)?; - store_bids(sk, my_bids).await.map_err(RgbSwapError::IO)?; + store_bids(sk, my_bids).await.map_err(RgbSwapError::IO)?; - rgb_swap_bid.tap_outpoint = Some(outpoint); - rgb_swap_bid.tap_amount = Some(amount); - rgb_swap_bid.tap_commit = Some(commit); - } else { - let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; - mark_transfer_offer(offer_id.clone(), consig_id.clone(), &mut my_offers) - .await - .map_err(RgbSwapError::Swap)?; + rgb_swap_bid.tap_outpoint = Some(outpoint); + rgb_swap_bid.tap_amount = Some(amount); + rgb_swap_bid.tap_commit = Some(commit); + offer_pub + } + RgbSwapStrategy::HotSwap => { + let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + mark_transfer_offer(offer_id.clone(), consig_id.clone(), &mut my_offers) + .await + .map_err(RgbSwapError::Swap)?; - store_offers(sk, my_offers.clone()) - .await - .map_err(RgbSwapError::IO)?; - - if let Some(list_offers) = my_offers.clone().offers.get(&contract_id) { - if let Some(my_offer) = list_offers.iter().find(|x| x.offer_id == offer_id) { - let mut rgb_wallet = rgb_account.wallets.get(RGB_DEFAULT_NAME).unwrap().clone(); - save_tap_commit_str( - &outpoint, - amount, - &commit, - &my_offer.terminal, - &mut rgb_wallet, - ); - rgb_account - .wallets - .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet); + store_offers(sk, my_offers.clone()) + .await + .map_err(RgbSwapError::IO)?; + + if let Some(list_offers) = my_offers.clone().offers.get(&contract_id) { + if let Some(my_offer) = list_offers.iter().find(|x| x.offer_id == offer_id) { + let mut rgb_wallet = rgb_account.wallets.get(RGB_DEFAULT_NAME).unwrap().clone(); + save_tap_commit_str( + &outpoint, + amount, + &commit, + &my_offer.terminal, + &mut rgb_wallet, + ); + rgb_account + .wallets + .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet); + } } + public } - } + invalid => return Err(RgbSwapError::WrongStrategy(invalid.to_string())), + }; let RgbExtractTransfer { strict, .. } = prebuild_extract_transfer(&final_consig) .map_err(|op| RgbSwapError::WrongConsigSwap(op.to_string()))?; - let counter_party = if presig { offer_pub } else { bid_pub }; rgb_swap_bid.transfer_id = Some(consig_id.clone()); rgb_swap_bid.transfer = Some(strict.to_hex()); rgb_swap_bid.swap_psbt = Some(final_psbt.clone()); @@ -1545,6 +1834,53 @@ pub async fn internal_replace_transfer( Ok(resp) } +pub async fn verify_auctions() -> Result, RgbSwapError> { + let utc = chrono::Local::now().naive_utc().timestamp(); + let auction_offers: Vec<_> = get_public_offers() + .await + .map_err(RgbSwapError::Auction)? + .into_iter() + .filter(|x| { + x.strategy == RgbSwapStrategy::Auction && x.expire_at.unwrap_or_default().sub(utc) <= 0 + }) + .collect(); + + let mut process = vec![]; + for offer in auction_offers { + let RgbOfferSwap { + offer_id, + bundle_id, + .. + } = offer.clone(); + if let Some(highest_bid) = get_auction_highest_bid(bundle_id.unwrap_or_default(), offer_id) + .await + .map_err(RgbSwapError::Auction)? + { + let RgbBidSwap { + bid_id, + offer_id, + swap_psbt, + transfer_id, + .. + } = highest_bid; + let request = PublishPsbtRequest { + psbt: swap_psbt.unwrap_or_default(), + }; + publish_psbt_file(request) + .await + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + + process.push(RgbSwapStatusResponse { + consig_id: transfer_id.unwrap_or_default(), + offer_id, + bid_id, + }) + } + } + + Ok(process) +} + pub async fn accept_transfer( sk: &str, request: AcceptRequest, @@ -1852,7 +2188,7 @@ pub async fn internal_swap_transfers( current_offers.retain(|x| x.offer_status != RgbOrderStatus::Fill); for offer in current_offers { - if let Ok(swaps_bids) = get_swap_bids_by_seller(sk, offer.clone()).await { + if let Ok(swaps_bids) = get_swap_bids_by_offer(sk, offer.clone()).await { my_swaps.extend(swaps_bids.clone()); for swap_bid in swaps_bids { diff --git a/src/rgb/auction.rs b/src/rgb/auction.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/rgb/cambria.rs b/src/rgb/cambria.rs index 8db20f67..9b3690aa 100644 --- a/src/rgb/cambria.rs +++ b/src/rgb/cambria.rs @@ -38,7 +38,7 @@ impl From for RgbAccountVersions { fn from(value: String) -> Self { match value.to_lowercase().as_str() { "v0" | "0" | "rgbst161" | "" => RgbAccountVersions::V0(RgbAccountV0::default()), - "v1" | "1" => RgbAccountVersions::V1(RgbAccountV1::default()), + "v10" | "v1" | "1" => RgbAccountVersions::V1(RgbAccountV1::default()), _ => RgbAccountVersions::Unknown, } } @@ -96,7 +96,7 @@ impl From for RgbtransferVersions { fn from(value: String) -> Self { match value.to_lowercase().as_str() { "v0" | "0" | "rgbst161" | "" => RgbtransferVersions::V0(RgbTransfersV0::default()), - "v1" | "1" => RgbtransferVersions::V1(RgbTransfersV1::default()), + "v10" | "v1" | "1" => RgbtransferVersions::V1(RgbTransfersV1::default()), _ => RgbtransferVersions::Unknown, } } diff --git a/src/rgb/carbonado.rs b/src/rgb/carbonado.rs index f446c22d..d794e807 100644 --- a/src/rgb/carbonado.rs +++ b/src/rgb/carbonado.rs @@ -5,12 +5,12 @@ use postcard::{from_bytes, to_allocvec}; use rgbstd::{persistence::Stock, stl::LIB_ID_RGB}; use strict_encoding::{StrictDeserialize, StrictSerialize}; -use crate::carbonado::server_store; +use crate::carbonado::{auctions_retrieve, auctions_store, marketplace_store}; use crate::rgb::crdt::{LocalRgbAccount, LocalRgbOffers, RawRgbAccount}; use crate::rgb::swap::{RgbBids, RgbOffers}; use crate::{ - carbonado::{retrieve, server_retrieve, store}, + carbonado::{marketplace_retrieve, retrieve, store}, rgb::{ cambria::{ModelVersion, RgbAccountVersions}, constants::RGB_STRICT_TYPE_VERSION, @@ -20,11 +20,12 @@ use crate::{ }; use super::cambria::RgbtransferVersions; +use super::crdt::LocalRgbAuctions; use super::structs::RgbTransfersV1; -use super::swap::{PublicRgbOffers, RgbBidSwap}; +use super::swap::{RgbAuctionSwaps, RgbBidSwap, RgbPublicSwaps}; -const RGB_ACCOUNT_VERSION: [u8; 2] = *b"v1"; -const RGB_TRANSFER_VERSION: [u8; 2] = *b"v1"; +const RGB_ACCOUNT_VERSION: [u8; 3] = *b"v10"; +const RGB_TRANSFER_VERSION: [u8; 3] = *b"v10"; #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] @@ -331,7 +332,7 @@ pub async fn cdrt_retrieve_wallets(sk: &str, name: &str) -> Result Result Result Result Result Result { + let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + let main_name = &format!("{hashed_name}.c15"); + let original_name = &format!("{hashed_name}-diff.c15"); + + let (data, _) = auctions_retrieve(bundle_id, main_name) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + if data.is_empty() { + Ok(LocalRgbAuctions { + version: automerge::AutoCommit::new().save(), + rgb_offers: RgbAuctionSwaps::default(), + }) + } else { + let mut original_version = automerge::AutoCommit::new(); + let rgb_offers: RgbAuctionSwaps = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + + reconcile(&mut original_version, rgb_offers.clone()) + .map_err(|op| StorageError::Reconcile(name.to_string(), op.to_string()))?; + + let mut fork_version = original_version.fork(); + + auctions_store( + bundle_id, + original_name, + &fork_version.save(), + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(LocalRgbAuctions { + version: fork_version.save(), rgb_offers, }) } @@ -417,7 +463,7 @@ pub async fn store_public_offers(name: &str, changes: &[u8]) -> Result<(), Stora let main_name = &format!("{hashed_name}.c15"); let original_name = &format!("{hashed_name}-diff.c15"); - let (original_bytes, _) = server_retrieve(original_name) + let (original_bytes, _) = marketplace_retrieve(original_name) .await .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; @@ -431,18 +477,60 @@ pub async fn store_public_offers(name: &str, changes: &[u8]) -> Result<(), Stora .merge(&mut fork_version) .map_err(|op| StorageError::MergeWrite(name.to_string(), op.to_string()))?; - let public_offers: PublicRgbOffers = hydrate(&original_version).unwrap(); + let public_offers: RgbPublicSwaps = hydrate(&original_version).unwrap(); let data = to_allocvec(&public_offers) .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; - server_store(main_name, &data, Some(RGB_STRICT_TYPE_VERSION.to_vec())) + marketplace_store(main_name, &data, Some(RGB_STRICT_TYPE_VERSION.to_vec())) .await .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; Ok(()) } +pub async fn store_auction_offers( + bundle_id: &str, + name: &str, + changes: &[u8], +) -> Result<(), StorageError> { + let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + let main_name = &format!("{hashed_name}.c15"); + let original_name = &format!("{hashed_name}-diff.c15"); + + let (original_bytes, _) = marketplace_retrieve(original_name) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + + let mut original_version = automerge::AutoCommit::load(&original_bytes) + .map_err(|op| StorageError::ForkRead(name.to_string(), op.to_string()))?; + + let mut fork_version = automerge::AutoCommit::load(changes) + .map_err(|op| StorageError::ChangesRetrieve(name.to_string(), op.to_string()))?; + + original_version + .merge(&mut fork_version) + .map_err(|op| StorageError::MergeWrite(name.to_string(), op.to_string()))?; + + let auction_offers: RgbAuctionSwaps = hydrate(&original_version).unwrap(); + let data = to_allocvec(&auction_offers) + .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; + + auctions_store( + bundle_id, + main_name, + &data, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(()) +} + pub async fn retrieve_swap_offer_bid( sk: &str, name: &str, @@ -464,7 +552,7 @@ pub async fn retrieve_swap_offer_bid( .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; if data.is_empty() { Ok(LocalRgbOfferBid { - doc: automerge::AutoCommit::new().save(), + version: automerge::AutoCommit::new().save(), rgb_bid: RgbBidSwap::default(), }) } else { @@ -488,7 +576,7 @@ pub async fn retrieve_swap_offer_bid( .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; Ok(LocalRgbOfferBid { - doc: fork_version.save(), + version: fork_version.save(), rgb_bid: rgb_offer_bid, }) } @@ -511,7 +599,7 @@ pub async fn store_swap_offer_bid( let main_name = &format!("{hashed_name}.c15"); let original_name = &format!("{hashed_name}-diff.c15"); - let (original_bytes, _) = server_retrieve(original_name) + let (original_bytes, _) = marketplace_retrieve(original_name) .await .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; diff --git a/src/rgb/crdt.rs b/src/rgb/crdt.rs index e3879672..0a78b2f6 100644 --- a/src/rgb/crdt.rs +++ b/src/rgb/crdt.rs @@ -12,9 +12,11 @@ use std::{ use crate::rgb::{ structs::{RgbAccountV0, RgbAccountV1}, - swap::{PublicRgbOffers, RgbBidSwap}, + swap::{RgbBidSwap, RgbPublicSwaps}, }; +use super::swap::RgbAuctionSwaps; + #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum RgbMergeError { @@ -297,7 +299,7 @@ impl RgbMerge for Utxo { #[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] #[display(doc_comments)] pub struct LocalRgbAccount { - pub doc: Vec, + pub version: Vec, pub rgb_account: RgbAccountV1, } @@ -310,13 +312,20 @@ pub struct LocalCopyData { #[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] #[display(doc_comments)] pub struct LocalRgbOffers { - pub doc: Vec, - pub rgb_offers: PublicRgbOffers, + pub version: Vec, + pub rgb_offers: RgbPublicSwaps, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] +#[display(doc_comments)] +pub struct LocalRgbAuctions { + pub version: Vec, + pub rgb_offers: RgbAuctionSwaps, } #[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] #[display(doc_comments)] pub struct LocalRgbOfferBid { - pub doc: Vec, + pub version: Vec, pub rgb_bid: RgbBidSwap, } diff --git a/src/rgb/fs.rs b/src/rgb/fs.rs index 923b7b44..6ed9382b 100644 --- a/src/rgb/fs.rs +++ b/src/rgb/fs.rs @@ -5,14 +5,16 @@ use crate::constants::storage_keys::{ }; use crate::rgb::{ carbonado::{ - cdrt_retrieve_wallets, cdrt_store_wallets, retrieve_bids as retrieve_rgb_bids, - retrieve_offers as retrieve_rgb_offers, + cdrt_retrieve_wallets, cdrt_store_wallets, + retrieve_auctions_offers as retrieve_rgb_auctions_offers, + retrieve_bids as retrieve_rgb_bids, retrieve_offers as retrieve_rgb_offers, retrieve_public_offers as retrieve_rgb_public_offers, retrieve_stock as retrieve_rgb_stock, retrieve_swap_offer_bid as retrieve_rgb_swap_offer_bid, retrieve_transfers as retrieve_rgb_transfers, retrieve_wallets, - store_bids as store_rgb_bids, store_offers as store_rgb_offers, - store_public_offers as store_rgb_public_offers, store_stock as store_rgb_stock, - store_swap_offer_bid, store_transfers as store_rgb_transfer, store_wallets, + store_auction_offers as store_rgb_auction_offers, store_bids as store_rgb_bids, + store_offers as store_rgb_offers, store_public_offers as store_rgb_public_offers, + store_stock as store_rgb_stock, store_swap_offer_bid, + store_transfers as store_rgb_transfer, store_wallets, }, crdt::LocalRgbAccount, crdt::{LocalRgbOfferBid, LocalRgbOffers}, @@ -20,6 +22,8 @@ use crate::rgb::{ swap::{RgbBids, RgbOffers}, }; +use super::crdt::LocalRgbAuctions; + #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum RgbPersistenceError { @@ -97,6 +101,17 @@ pub async fn retrieve_public_offers() -> Result Result { + let stock = retrieve_rgb_auctions_offers(bundle_id, name) + .await + .map_err(|op| RgbPersistenceError::RetrievePublicOffers(op.to_string()))?; + + Ok(stock) +} + pub async fn retrieve_swap_offer_bid( sk: &str, name: &str, @@ -203,6 +218,16 @@ pub async fn store_public_offers(changes: Vec) -> Result<(), RgbPersistenceE .map_err(|op| RgbPersistenceError::WriteRgbPublicOffers(op.to_string())) } +pub async fn store_auction_offers( + bundle_id: &str, + name: &str, + changes: Vec, +) -> Result<(), RgbPersistenceError> { + store_rgb_auction_offers(bundle_id, name, &changes) + .await + .map_err(|op| RgbPersistenceError::WriteRgbPublicOffers(op.to_string())) +} + pub async fn store_stock_account( sk: &str, stock: Stock, diff --git a/src/rgb/swap.rs b/src/rgb/swap.rs index f05c7467..4b0f929b 100644 --- a/src/rgb/swap.rs +++ b/src/rgb/swap.rs @@ -1,10 +1,10 @@ #![allow(deprecated)] use super::{ constants::LIB_NAME_BITMASK, - crdt::{LocalRgbOfferBid, LocalRgbOffers}, + crdt::{LocalRgbAuctions, LocalRgbOfferBid, LocalRgbOffers}, fs::{ - retrieve_public_offers, retrieve_swap_offer_bid, store_public_offers, store_swap_bids, - RgbPersistenceError, + retrieve_auctions_offers, retrieve_public_offers, retrieve_swap_offer_bid, + store_auction_offers, store_public_offers, store_swap_bids, RgbPersistenceError, }, }; use crate::{structs::AllocationDetail, validators::RGBContext}; @@ -65,6 +65,38 @@ pub enum RgbOrderStatus { Fill, } +#[derive( + Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Default, Reconcile, Hydrate, Display, +)] +#[serde(rename_all = "camelCase")] +#[display(inner)] +pub enum RgbSwapStrategy { + #[default] + #[serde(rename = "auction")] + Auction, + #[serde(rename = "p2p")] + P2P, + #[serde(rename = "hotswap")] + HotSwap, +} + +#[derive(Clone, Debug, Display, Default, Error)] +#[display(doc_comments)] +pub struct RgbOfferOptions { + pub bundle_id: Option, +} + +impl RgbOfferOptions { + pub fn with_bundle_id(secret: String) -> Self { + let secp = Secp256k1::new(); + let secret = hex::decode(secret).expect("cannot decode hex sk in new RgbOffer"); + let secret_key = SecretKey::from_slice(&secret).expect("error parsing sk in new RgbOffer"); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let bundle_id = Some(public_key.to_hex()); + Self { bundle_id } + } +} + #[derive(Clone, Serialize, Deserialize, Validate, Reconcile, Hydrate, Debug, Display, Default)] #[garde(context(RGBContext))] #[display("{offer_id} / {contract_id}:{asset_amount} / {bitcoin_price}")] @@ -90,12 +122,14 @@ pub struct RgbOffer { pub seller_psbt: String, #[garde(ascii)] pub seller_address: String, - #[garde(skip)] - pub expire_at: Option, #[garde(ascii)] pub public: String, #[garde(skip)] - pub presig: bool, + pub strategy: RgbSwapStrategy, + #[garde(skip)] + pub expire_at: Option, + #[garde(skip)] + pub bundle_id: Option, #[garde(skip)] pub transfer_id: Option, } @@ -107,28 +141,21 @@ impl RgbOffer { contract_id: String, iface: String, allocations: Vec, + asset_amount: u64, asset_precision: u8, seller_address: AddressCompat, bitcoin_price: u64, psbt: String, - presig: bool, terminal: String, + strategy: RgbSwapStrategy, expire_at: Option, + bundle_id: Option, ) -> Self { let secp = Secp256k1::new(); let secret = hex::decode(secret).expect("cannot decode hex sk in new RgbOffer"); let secret_key = SecretKey::from_slice(&secret).expect("error parsing sk in new RgbOffer"); let public_key = PublicKey::from_secret_key(&secp, &secret_key); - let asset_amount = allocations - .clone() - .into_iter() - .map(|a| match a.value { - crate::structs::AllocationValue::Value(amount) => amount, - crate::structs::AllocationValue::UDA(_) => 1, - }) - .sum(); - let mut asset_utxos: Vec = allocations.into_iter().map(|a| a.utxo).collect(); asset_utxos.sort(); @@ -152,9 +179,10 @@ impl RgbOffer { seller_psbt: psbt, seller_address: seller_address.to_string(), public: public_key.to_hex(), - presig, expire_at, terminal, + strategy, + bundle_id, ..Default::default() } } @@ -181,12 +209,14 @@ pub struct RgbOfferSwap { pub seller_psbt: String, #[garde(ascii)] pub seller_address: String, - #[garde(skip)] - pub expire_at: Option, #[garde(ascii)] pub public: String, #[garde(skip)] - pub presig: bool, + pub strategy: RgbSwapStrategy, + #[garde(skip)] + pub bundle_id: Option, + #[garde(skip)] + pub expire_at: Option, } impl From for RgbOfferSwap { @@ -200,9 +230,10 @@ impl From for RgbOfferSwap { seller_psbt, seller_address, public, - expire_at, - presig, asset_precision, + strategy, + expire_at, + bundle_id, .. } = value; @@ -215,9 +246,10 @@ impl From for RgbOfferSwap { seller_psbt, seller_address, public, - expire_at, - presig, + strategy, asset_precision, + expire_at, + bundle_id, } } } @@ -418,17 +450,165 @@ pub struct RgbOffers { pub bids: BTreeMap>, } +impl RgbOffers { + pub fn save_offer(mut self, contract_id: AssetId, offer: RgbOffer) -> Self { + if let Some(offers) = self.offers.get(&contract_id) { + let mut available_offers = offers.to_owned(); + if let Some(position) = available_offers + .iter() + .position(|x| x.offer_id == offer.offer_id) + { + available_offers.remove(position); + available_offers.insert(position, offer.clone()); + } else { + available_offers.push(offer.clone()); + } + + available_offers.push(offer.clone()); + self.offers.insert(contract_id, available_offers); + } else { + self.offers.insert(contract_id, vec![offer.clone()]); + } + self + } +} + #[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] pub struct RgbBids { pub bids: BTreeMap>, } +impl RgbBids { + pub fn save_bid(mut self, contract_id: AssetId, bid: RgbBid) -> Self { + if let Some(offers) = self.bids.get(&contract_id) { + let mut available_bids: Vec = offers.to_owned(); + if let Some(position) = available_bids.iter().position(|x| x.bid_id == bid.bid_id) { + available_bids.remove(position); + available_bids.insert(position, bid.clone()); + } else { + available_bids.push(bid.clone()); + } + + available_bids.push(bid.clone()); + self.bids.insert(contract_id, available_bids); + } else { + self.bids.insert(contract_id, vec![bid.clone()]); + } + self + } +} + #[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] -pub struct PublicRgbOffers { +pub struct RgbPublicSwaps { pub offers: BTreeMap>, pub bids: BTreeMap>, } +impl RgbPublicSwaps { + pub fn get_offer(self, offer_id: OfferId) -> Option { + let mut public_offers = vec![]; + for offers in self.offers.values() { + public_offers.extend(offers); + } + + public_offers + .into_iter() + .find(|x| x.offer_id == offer_id) + .cloned() + } + + pub fn save_offer(mut self, contract_id: AssetId, offer: RgbOfferSwap) -> Self { + if let Some(offers) = self.offers.get(&contract_id) { + let mut available_offers = offers.to_owned(); + if let Some(position) = available_offers + .iter() + .position(|x| x.offer_id == offer.offer_id) + { + available_offers.remove(position); + available_offers.insert(position, offer.clone()); + } else { + available_offers.push(offer.clone()); + } + + available_offers.push(offer.clone()); + self.offers.insert(contract_id, available_offers); + } else { + self.offers.insert(contract_id, vec![offer.clone()]); + } + self + } + + pub fn save_bid(mut self, offer_id: OfferId, bid: RgbBidSwap) -> Self { + let new_public_bid = PublicRgbBid::from(bid); + let PublicRgbBid { bid_id, .. } = new_public_bid.clone(); + if let Some(bids) = self.bids.get(&offer_id) { + let mut available_bids = bids.to_owned(); + available_bids.insert(bid_id, new_public_bid); + self.bids.insert(offer_id.clone(), available_bids); + } else { + self.bids + .insert(offer_id.clone(), bmap! { bid_id => new_public_bid }); + } + self + } +} + +#[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] +pub struct RgbAuctionSwaps { + pub bundle_id: String, + pub items: Vec, + pub bids: BTreeMap>, +} + +impl RgbAuctionSwaps { + pub fn is_current_offer(self, offer_id: OfferId) -> bool { + if let Some(offer) = self.items.first() { + offer.offer_id.eq(&offer_id) + } else { + false + } + } + + pub fn current_offer(self) -> Option { + self.items.first().cloned() + } + + pub fn save_bid(mut self, offer_id: OfferId, bid: RgbBidSwap) -> Self { + let available_bids = if let Some(bids) = self.bids.get(&offer_id) { + let mut available_bids = bids.to_owned(); + available_bids.push(bid); + available_bids + } else { + vec![bid] + }; + + self.bids.insert(offer_id.clone(), available_bids); + self + } + + pub fn save_offers(mut self, offers: Vec) -> Self { + for offer in offers.into_iter() { + self = self.save_offer(offer); + } + self + } + + fn save_offer(mut self, offer: RgbOfferSwap) -> Self { + let mut available_offers = self.items.clone(); + if let Some(position) = available_offers + .iter() + .position(|x| x.offer_id == offer.offer_id) + { + available_offers.remove(position); + available_offers.insert(position, offer.clone()); + } else { + available_offers.push(offer.clone()); + } + self.items = available_offers; + self + } +} + #[derive(Clone, Eq, PartialEq, Debug, Display, From, Error)] #[display(doc_comments)] pub enum RgbOfferErrors { @@ -440,12 +620,25 @@ pub enum RgbOfferErrors { NoOffer(String), /// Bid #{0} is not found in public orderbook. NoBid(String), + /// Collection offers empty + NoBundle, /// Occurs an error in merge step. {0} AutoMerge(String), } +pub async fn get_public_offers() -> Result, RgbOfferErrors> { + let LocalRgbOffers { rgb_offers, .. } = + retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; + + let mut public_offers = vec![]; + for offers in rgb_offers.offers.values() { + public_offers.extend(offers.iter().cloned()); + } + Ok(public_offers) +} + pub async fn get_public_offer(offer_id: OfferId) -> Result { - let LocalRgbOffers { doc: _, rgb_offers } = + let LocalRgbOffers { rgb_offers, .. } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; let mut public_offers = vec![]; @@ -461,11 +654,28 @@ pub async fn get_public_offer(offer_id: OfferId) -> Result Result, RgbOfferErrors> { + let file_name = format!("bundle:{bundle_id}"); + + let LocalRgbAuctions { rgb_offers, .. } = retrieve_auctions_offers(bundle_id, &file_name) + .await + .map_err(RgbOfferErrors::IO)?; + + if !rgb_offers.clone().is_current_offer(offer_id.clone()) { + return Err(RgbOfferErrors::NoOffer(offer_id)); + } + + Ok(rgb_offers.current_offer()) +} + pub async fn get_public_bid( offer_id: OfferId, bid_id: BidId, ) -> Result { - let LocalRgbOffers { doc: _, rgb_offers } = + let LocalRgbOffers { rgb_offers, .. } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; let public_bids = match rgb_offers.bids.get(&offer_id) { @@ -481,7 +691,7 @@ pub async fn get_public_bid( Ok(public_bid) } -pub async fn get_swap_bids_by_seller( +pub async fn get_swap_bids_by_offer( sk: &str, offer: RgbOffer, ) -> Result, RgbOfferErrors> { @@ -491,7 +701,7 @@ pub async fn get_swap_bids_by_seller( .. } = offer; - let LocalRgbOffers { doc: _, rgb_offers } = + let LocalRgbOffers { rgb_offers, .. } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; let public_bids: Vec = match rgb_offers.bids.get(&offer_id) { @@ -521,7 +731,7 @@ pub async fn get_swap_bids_by_seller( Ok(swap_bids) } -pub async fn get_swap_bid( +pub async fn get_swap_bid_by_seller( sk: &str, offer_id: String, bid_id: BidId, @@ -574,72 +784,96 @@ pub async fn get_swap_bid_by_buyer( Ok(rgb_bid) } +pub async fn get_auction_highest_bid( + bundle_id: String, + offer_id: OfferId, +) -> Result, RgbOfferErrors> { + let file_name = format!("bundle:{bundle_id}"); + + let LocalRgbAuctions { rgb_offers, .. } = retrieve_auctions_offers(&bundle_id, &file_name) + .await + .map_err(RgbOfferErrors::IO)?; + + if !rgb_offers.clone().is_current_offer(offer_id.clone()) { + return Err(RgbOfferErrors::NoOffer(offer_id.clone())); + } + + let highest_bid = if let Some(bids) = rgb_offers.bids.get(&offer_id) { + bids.iter().max_by_key(|x| x.bitcoin_amount).cloned() + } else { + None + }; + + Ok(highest_bid) +} + pub async fn publish_public_offer(new_offer: RgbOfferSwap) -> Result<(), RgbOfferErrors> { let LocalRgbOffers { - doc, mut rgb_offers, + version, } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; - let mut local_copy = automerge::AutoCommit::load(&doc) + let mut current_version = automerge::AutoCommit::load(&version) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; - if let Some(offers) = rgb_offers.offers.get(&new_offer.contract_id) { - let mut available_offers = offers.to_owned(); - if let Some(position) = available_offers - .iter() - .position(|x| x.offer_id == new_offer.offer_id) - { - available_offers.remove(position); - available_offers.insert(position, new_offer.clone()); - } else { - available_offers.push(new_offer.clone()); - } - rgb_offers - .offers - .insert(new_offer.clone().contract_id, available_offers); - } else { - rgb_offers - .offers - .insert(new_offer.clone().contract_id, vec![new_offer]); - } + let contract_id = new_offer.contract_id.clone(); + rgb_offers = rgb_offers.save_offer(contract_id, new_offer); - // TODO: Add change verification (accept only addition operation) - reconcile(&mut local_copy, rgb_offers) + reconcile(&mut current_version, rgb_offers) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; - store_public_offers(local_copy.save()) + store_public_offers(current_version.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(()) +} + +pub async fn publish_auction_offers(new_offers: Vec) -> Result<(), RgbOfferErrors> { + let RgbOfferSwap { bundle_id, .. } = new_offers[0].clone(); + let bundle_id = bundle_id.unwrap_or_default(); + let file_name = format!("bundle:{bundle_id}"); + + let LocalRgbAuctions { + mut rgb_offers, + version, + } = retrieve_auctions_offers(&bundle_id, &file_name) .await .map_err(RgbOfferErrors::IO)?; + let mut current_version = automerge::AutoCommit::load(&version) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + rgb_offers = rgb_offers.save_offers(new_offers); + reconcile(&mut current_version, rgb_offers.clone()) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + if let Some(new_offer) = rgb_offers.current_offer() { + store_auction_offers(&bundle_id, &file_name, current_version.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + publish_public_offer(new_offer).await?; + } else { + return Err(RgbOfferErrors::NoBundle); + } + Ok(()) } pub async fn publish_public_bid(new_bid: RgbBidSwap) -> Result<(), RgbOfferErrors> { - let RgbBidSwap { - bid_id, offer_id, .. - } = new_bid.clone(); + let RgbBidSwap { offer_id, .. } = new_bid.clone(); let _ = get_public_offer(offer_id.clone()).await?; let LocalRgbOffers { - doc, mut rgb_offers, + version, } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; - let mut local_copy = automerge::AutoCommit::load(&doc) + let mut local_copy = automerge::AutoCommit::load(&version) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; - let new_public_bid = PublicRgbBid::from(new_bid); - if let Some(bids) = rgb_offers.bids.get(&offer_id) { - let mut available_bids = bids.to_owned(); - available_bids.insert(bid_id, new_public_bid); - rgb_offers.bids.insert(offer_id.clone(), available_bids); - } else { - rgb_offers - .bids - .insert(offer_id.clone(), bmap! { bid_id => new_public_bid }); - } - - // TODO: Add change verification (accept only addition operation) + rgb_offers = rgb_offers.save_bid(offer_id, new_bid); reconcile(&mut local_copy, rgb_offers) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; @@ -650,6 +884,35 @@ pub async fn publish_public_bid(new_bid: RgbBidSwap) -> Result<(), RgbOfferError Ok(()) } +pub async fn publish_auction_bid(new_bid: RgbBidSwap) -> Result<(), RgbOfferErrors> { + let RgbBidSwap { offer_id, .. } = new_bid.clone(); + let RgbOfferSwap { bundle_id, .. } = get_public_offer(offer_id.clone()).await?; + let bundle_id = bundle_id.unwrap_or_default(); + let file_name = format!("bundle:{bundle_id}"); + + let LocalRgbAuctions { + mut rgb_offers, + version, + } = retrieve_auctions_offers(&bundle_id, &file_name) + .await + .map_err(RgbOfferErrors::IO)?; + + let mut local_copy = automerge::AutoCommit::load(&version) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + rgb_offers = rgb_offers.save_bid(offer_id.clone(), new_bid.clone()); + reconcile(&mut local_copy, rgb_offers) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_auction_offers(&bundle_id, &file_name, local_copy.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + publish_public_bid(new_bid).await?; + + Ok(()) +} + pub async fn publish_swap_bid( sk: &str, offer_pub: &str, @@ -669,11 +932,12 @@ pub async fn publish_swap_bid( let share_sk = SharedSecret::new(&public_key, &secret_key); let share_sk = share_sk.display_secret().to_string(); let file_name = format!("{offer_id}-{bid_id}"); - let LocalRgbOfferBid { doc, .. } = retrieve_swap_offer_bid(&share_sk, &file_name, expire_at) - .await - .map_err(RgbOfferErrors::IO)?; + let LocalRgbOfferBid { version, .. } = + retrieve_swap_offer_bid(&share_sk, &file_name, expire_at) + .await + .map_err(RgbOfferErrors::IO)?; - let mut local_copy = automerge::AutoCommit::load(&doc) + let mut local_copy = automerge::AutoCommit::load(&version) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; reconcile(&mut local_copy, new_bid).map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; @@ -687,11 +951,11 @@ pub async fn publish_swap_bid( pub async fn remove_public_offers(offers: Vec) -> Result<(), RgbOfferErrors> { let LocalRgbOffers { - doc, mut rgb_offers, + version, } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; - let mut local_copy = automerge::AutoCommit::load(&doc) + let mut local_copy = automerge::AutoCommit::load(&version) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; let current_public_offers = rgb_offers.clone(); diff --git a/src/structs.rs b/src/structs.rs index f4842e19..d29681f2 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -14,7 +14,7 @@ use rgbstd::interface::rgb21::Allocation as AllocationUDA; use crate::{ rgb::{ structs::MediaMetadata, - swap::{PublicRgbBid, RgbBid, RgbOffer, RgbOfferSwap}, + swap::{PublicRgbBid, RgbBid, RgbOffer, RgbOfferSwap, RgbSwapStrategy}, }, validators::{ verify_descriptor, verify_media_request, verify_rgb_invoice, verify_tapret_seal, @@ -1338,7 +1338,7 @@ pub struct RgbOfferRequest { #[garde(length(min = 0, max = 999))] pub bitcoin_changes: Vec, #[garde(skip)] - pub presig: bool, + pub strategy: RgbSwapStrategy, #[garde(skip)] pub expire_at: Option, } @@ -1414,6 +1414,26 @@ pub struct RgbBidRequest { pub fee: PsbtFeeRequest, } +impl From for RgbBidRequest { + fn from(value: RgbAuctionBidRequest) -> Self { + let RgbAuctionBidRequest { + offer_id, + asset_amount, + descriptor, + change_terminal, + fee, + .. + } = value; + Self { + offer_id, + asset_amount, + descriptor, + change_terminal, + fee, + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] #[serde(rename_all = "camelCase")] #[display("{bid_id} ~ {offer_id}")] @@ -1430,6 +1450,44 @@ pub struct RgbBidResponse { pub fee_value: u64, } +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] +#[garde(context(RGBContext))] +#[serde(rename_all = "camelCase")] +#[display("{offer_id}:{asset_amount} ** {change_terminal}")] +pub struct RgbAuctionBidRequest { + /// The Offer ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: String, + /// Asset Amount + #[garde(skip)] + pub asset_amount: String, + /// Universal Descriptor + #[garde(custom(verify_descriptor))] + pub descriptor: SecretString, + /// Bitcoin Terminal Change + #[garde(ascii)] + pub change_terminal: String, + /// Descriptors to Sign + #[garde(skip)] + pub sign_keys: Vec, + /// Bitcoin Fee + #[garde(dive)] + pub fee: PsbtFeeRequest, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{bid_id} ~ {offer_id}")] +pub struct RgbAuctionBidResponse { + /// The Bid ID + pub bid_id: String, + /// The Offer ID + pub offer_id: String, + /// Fee Value + pub fee_value: u64, +} + #[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] #[garde(context(RGBContext))] #[serde(rename_all = "camelCase")] @@ -1460,6 +1518,18 @@ pub struct RgbSwapResponse { pub final_psbt: String, } +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{consig_id}")] +pub struct RgbSwapStatusResponse { + /// Transfer ID + pub consig_id: String, + /// Offer ID + pub offer_id: String, + /// Bid ID + pub bid_id: String, +} + #[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] #[serde(rename_all = "camelCase")] #[display("{offers:?}")] diff --git a/tests/rgb/integration/swaps.rs b/tests/rgb/integration/swaps.rs index baf81012..a42fdba1 100644 --- a/tests/rgb/integration/swaps.rs +++ b/tests/rgb/integration/swaps.rs @@ -10,7 +10,7 @@ use bitmask_core::{ rgb::{ accept_transfer, create_buyer_bid, create_seller_offer, create_swap_transfer, create_watcher, get_contract, import as import_contract, structs::ContractAmount, - update_seller_offer, verify_transfers, + swap::RgbSwapStrategy, update_seller_offer, verify_transfers, }, structs::{ AcceptRequest, AssetType, ImportRequest, IssueResponse, PsbtFeeRequest, PublishPsbtRequest, @@ -21,7 +21,7 @@ use bitmask_core::{ }; #[tokio::test] -async fn create_scriptless_swap() -> anyhow::Result<()> { +async fn create_hotswap_swap() -> anyhow::Result<()> { // 1. Initial Setup let seller_keys = new_mnemonic(&SecretString("".to_string())).await?; let buyer_keys = new_mnemonic(&SecretString("".to_string())).await?; @@ -131,7 +131,7 @@ async fn create_scriptless_swap() -> anyhow::Result<()> { // 5. Create Seller Swap Side let contract_amount = supply - 1; - let bitcoin_price: u64 = 100000; + let bitcoin_price: u64 = 100_000; let seller_asset_desc = seller_keys.public.rgb_assets_descriptor_xpub.clone(); let expire_at = (chrono::Local::now() + chrono::Duration::minutes(5)) .naive_utc() @@ -146,8 +146,8 @@ async fn create_scriptless_swap() -> anyhow::Result<()> { descriptor: SecretString(seller_asset_desc), change_terminal: "/20/1".to_string(), bitcoin_changes: vec![], + strategy: RgbSwapStrategy::HotSwap, expire_at: Some(expire_at), - presig: false, }; let seller_swap_resp = create_seller_offer(&seller_sk, seller_swap_req).await; @@ -249,7 +249,7 @@ async fn create_scriptless_swap() -> anyhow::Result<()> { } #[tokio::test] -async fn create_scriptless_swap_for_uda() -> anyhow::Result<()> { +async fn create_hotswap_swap_for_uda() -> anyhow::Result<()> { // 1. Initial Setup let seller_keys = new_mnemonic(&SecretString("".to_string())).await?; let buyer_keys = new_mnemonic(&SecretString("".to_string())).await?; @@ -374,8 +374,8 @@ async fn create_scriptless_swap_for_uda() -> anyhow::Result<()> { descriptor: SecretString(seller_asset_desc), change_terminal: "/21/1".to_string(), bitcoin_changes: vec![], + strategy: RgbSwapStrategy::HotSwap, expire_at: Some(expire_at), - presig: false, }; let seller_swap_resp = create_seller_offer(&seller_sk, seller_swap_req).await; @@ -476,7 +476,7 @@ async fn create_scriptless_swap_for_uda() -> anyhow::Result<()> { } #[tokio::test] -async fn create_presig_scriptless_swap() -> anyhow::Result<()> { +async fn create_p2p_swap() -> anyhow::Result<()> { // 1. Initial Setup let seller_keys = new_mnemonic(&SecretString("".to_string())).await?; let buyer_keys = new_mnemonic(&SecretString("".to_string())).await?; @@ -602,7 +602,7 @@ async fn create_presig_scriptless_swap() -> anyhow::Result<()> { change_terminal: "/20/1".to_string(), bitcoin_changes: vec![], expire_at: Some(expire_at), - presig: true, + strategy: RgbSwapStrategy::P2P, }; let seller_swap_resp = create_seller_offer(&seller_sk, seller_swap_req).await; diff --git a/tests/rgb/web/swaps.rs b/tests/rgb/web/swaps.rs index 8667a70c..d62d53ee 100644 --- a/tests/rgb/web/swaps.rs +++ b/tests/rgb/web/swaps.rs @@ -9,7 +9,7 @@ use bitmask_core::rgb::structs::ContractAmount; use bitmask_core::web::constants::sleep; use bitmask_core::{ debug, info, - rgb::{prefetch::prefetch_resolver_txs, resolvers::ExplorerResolver}, + rgb::{prefetch::prefetch_resolver_txs, resolvers::ExplorerResolver, swap::RgbSwapStrategy}, structs::{ AssetType, BatchRgbTransferResponse, ContractResponse, ContractsResponse, DecryptedWalletData, FullRgbTransferRequest, FundVaultDetails, ImportRequest, @@ -279,7 +279,7 @@ async fn create_transfer_swap_flow() { change_terminal: "/20/1".to_string(), bitcoin_changes: vec![], expire_at: Some(expire_at), - presig: false, + strategy: RgbSwapStrategy::HotSwap, }; let sender_swap_req = serde_wasm_bindgen::to_value(&sender_swap_req).expect("");