diff --git a/lib/web/rgb.ts b/lib/web/rgb.ts index d8bb10d0..c9a539c4 100644 --- a/lib/web/rgb.ts +++ b/lib/web/rgb.ts @@ -157,12 +157,6 @@ export const createOffer = async ( ): Promise => JSON.parse(await BMC.create_offer(nostrHexSk, request)); -export const createAuctionBid = async ( - nostrHexSk: string, - request: RgbAuctionBidRequest -): Promise => - JSON.parse(await BMC.create_auction_bid(nostrHexSk, request)); - export const createBid = async ( nostrHexSk: string, request: RgbBidRequest @@ -175,11 +169,24 @@ export const createSwap = async ( ): Promise => JSON.parse(await BMC.create_swap(nostrHexSk, request)); +export const createAuctionOffer = async ( + nostrHexSk: string, + request: RgbAuctionOfferRequest +): Promise => + JSON.parse(await BMC.create_auction_offers(nostrHexSk, request)); + + +export const createAuctionBid = async ( + nostrHexSk: string, + request: RgbAuctionBidRequest +): Promise => + JSON.parse(await BMC.create_auction_bid(nostrHexSk, request)); + export const finishAuction = async ( nostrHexSk: string, request: string ): Promise => - JSON.parse(await BMC.finish_auction(nostrHexSk, request)); + JSON.parse(await BMC.finish_auction_offers(nostrHexSk, request)); export const listAuctions = async (): Promise => JSON.parse(await BMC.list_auctions()); @@ -817,11 +824,38 @@ export interface RgbSwapStrategy { p2p?: string, hotswap?: string, } + +export interface RgbAuctionStrategy { + auction?: string, + airdrop?: string, +} + export interface RgbAuctionOfferRequest { + offers: RgbOfferRequest[], + signKeys: string[], - /// List of Offers - offers: RgbOfferRequest[], + strategy: RgbAuctionStrategy, + + fee?: PsbtFeeRequest +} + +export interface RgbOfferUpdateRequest { + /// The Contract ID + contract_id: string, + /// The Offer ID + offer_id: string, + // Swap PSBT + offer_psbt: string +} + +export interface RgbOfferUpdateResponse { + /// The Contract ID + contract_id: string, + /// The Offer ID + offer_id: string, + /// Updated? + updated: boolean } export interface RgbAuctionBidRequest { diff --git a/src/bin/bitmaskd.rs b/src/bin/bitmaskd.rs index fb65e803..2d15ba8b 100644 --- a/src/bin/bitmaskd.rs +++ b/src/bin/bitmaskd.rs @@ -25,7 +25,10 @@ use axum::{ use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, SecretKey}; use bitmask_core::{ bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, - carbonado::{handle_file, marketplace_retrieve, marketplace_store, metrics, store}, + carbonado::{ + auctions_retrieve, auctions_store, handle_file, marketplace_retrieve, marketplace_store, + metrics, store, + }, constants::{ get_marketplace_nostr_key, get_marketplace_seed, get_network, get_udas_utxo, switch_network, }, @@ -34,11 +37,12 @@ use bitmask_core::{ proxy_media_data_store, proxy_media_retrieve, proxy_metadata_retrieve, }, rgb::{ - accept_transfer, clear_watcher as rgb_clear_watcher, create_invoice, create_psbt, - create_watcher, full_transfer_asset, get_contract, import as rgb_import, issue_contract, - list_contracts, list_interfaces, list_schemas, list_transfers as list_rgb_transfers, - reissue_contract, remove_transfer as remove_rgb_transfer, - save_transfer as save_rgb_transfer, + accept_transfer, + carbonado::retrieve_auctions_offers, + clear_watcher as rgb_clear_watcher, create_invoice, create_psbt, create_watcher, + full_transfer_asset, get_contract, import as rgb_import, issue_contract, list_contracts, + list_interfaces, list_schemas, list_transfers as list_rgb_transfers, reissue_contract, + remove_transfer as remove_rgb_transfer, save_transfer as save_rgb_transfer, structs::{ RgbProxyConsigCarbonadoReq, RgbProxyConsigFileReq, RgbProxyConsigUpload, RgbProxyMediaCarbonadoReq, RgbProxyMediaFileReq, @@ -687,53 +691,68 @@ 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, +async fn rgb_retrieve_auction( + Path((bundle_id, name)): Path<(String, String)>, ) -> Result { - info!("GET /auction/{offer_id}"); - Ok((StatusCode::OK, Json(""))) -} + info!("GET /auction/{bundle_id}"); -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(""))) + let result = auctions_retrieve(&bundle_id, &name).await; + let cc = CacheControl::new().with_no_cache(); + match result { + Ok((bytes, _)) => { + debug!("read {0} bytes.", bytes.len()); + Ok((StatusCode::OK, TypedHeader(cc), bytes)) + } + Err(e) => { + debug!("file read error {0} .Details: {1}.", name, e.to_string()); + Ok((StatusCode::OK, TypedHeader(cc), Vec::::new())) + } + } } -async fn rgb_auction_destroy_offer( - TypedHeader(_auth): TypedHeader>, - Path(offer_id): Path, +async fn rgb_store_auction( + Path((bundle_id, name)): Path<(String, String)>, + body: Bytes, ) -> Result { - info!("DELETE /auction/{offer_id}"); - Ok((StatusCode::OK, Json(""))) -} + info!("POST /auction/{bundle_id}"); + let (filepath, encoded) = auctions_store(&bundle_id, &name, &body, None).await?; -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(""))) -} + match OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&filepath) + { + Ok(file) => { + let present_header = match carbonado::file::Header::try_from(&file) { + Ok(header) => header, + _ => carbonado::file::Header::try_from(&body)?, + }; + let present_len = present_header.encoded_len - present_header.padding_len; + debug!("present_len: {present_len}"); + let resp = fs::write(&filepath, &encoded).await; + debug!("file override status {}", resp.is_ok()); + } + Err(err) => match err.kind() { + ErrorKind::NotFound => { + debug!("no file found, writing 0 bytes."); + fs::write(&filepath, &body).await?; + } + _ => { + error!("error in POST /carbonado/server/{name}: {err}"); + return Err(err.into()); + } + }, + } -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(""))) + let cc = CacheControl::new().with_no_cache(); + Ok((StatusCode::OK, TypedHeader(cc))) } -async fn rgb_auction_destroy_bid( - TypedHeader(_auth): TypedHeader>, - Path((offer_id, bid_id)): Path<(String, String)>, +async fn rgb_destroy_auction( + Path((bundle_id, _name)): Path<(String, String)>, ) -> Result { - info!("DELETE /auction/{offer_id}/{bid_id}"); + info!("DELETE /auction/{bundle_id}"); Ok((StatusCode::OK, Json(""))) } @@ -862,18 +881,9 @@ 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("/auction/:bundle_id/:name", get(rgb_retrieve_auction)) + .route("/auction/:bundle_id/:name", post(rgb_store_auction)) + .route("/auction/:bundle_id/:name", delete(rgb_destroy_auction)) .route("/metrics.json", get(json_metrics)) .route("/metrics.csv", get(csv_metrics)); diff --git a/src/carbonado.rs b/src/carbonado.rs index a3da1027..9b0d1beb 100644 --- a/src/carbonado.rs +++ b/src/carbonado.rs @@ -414,12 +414,36 @@ mod client { } pub async fn auctions_store( - _bundle_id: &str, - _name: &str, - _input: &[u8], + bundle_id: &str, + name: &str, + input: &[u8], _metadata: Option>, ) -> Result<(), CarbonadoError> { - todo!() + let body = Arc::new(input.to_vec()); + let network = NETWORK.read().await.to_string(); + let endpoints = CARBONADO_ENDPOINT.read().await.to_string(); + let endpoints: Vec<&str> = endpoints.split(',').collect(); + let requests = Array::new(); + + for endpoint in endpoints { + let url = format!("{endpoint}/auction/{bundle_id}/{network}-{name}"); + let fetch_fn = future_to_promise(fetch_post(url, body.clone())); + requests.push(&fetch_fn); + } + + let results = JsFuture::from(Promise::all_settled(&JsValue::from(requests))) + .await + .map_err(js_to_error)?; + + info!(format!("Store results: {results:?}")); + + let results = serde_wasm_bindgen::from_value::>(results)?; + let success = results.iter().any(|result| result.value == 200.0); + if success { + Ok(()) + } else { + Err(CarbonadoError::AllEndpointsFailed) + } } pub async fn retrieve_metadata(sk: &str, name: &str) -> Result { @@ -535,10 +559,28 @@ mod client { } pub async fn auctions_retrieve( - _bundle_id: &str, - _name: &str, + bundle_id: &str, + name: &str, ) -> Result<(Vec, Option>), CarbonadoError> { - todo!() + let network = NETWORK.read().await.to_string(); + let endpoints = CARBONADO_ENDPOINT.read().await.to_string(); + let endpoints: Vec<&str> = endpoints.split(',').collect(); + + let requests = Array::new(); + for endpoint in endpoints.iter() { + let url = format!("{endpoint}/auction/{bundle_id}/{network}-{name}"); + let fetch_fn = future_to_promise(fetch_get_byte_array(url)); + requests.push(&fetch_fn); + } + + let result = JsFuture::from(Promise::any(&JsValue::from(requests))) + .await + .map_err(js_to_error)?; + + let array = Uint8Array::from(result); + let encoded = array.to_vec(); + + Ok((encoded.to_vec(), None)) } async fn fetch_post(url: String, body: Arc>) -> Result { diff --git a/src/rgb.rs b/src/rgb.rs index 7bb3abb9..5c17efac 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -118,12 +118,13 @@ use self::{ RgbTransferV1, RgbTransfersV1, }, swap::{ - complete_bid, complete_offer, get_auction_highest_bids, get_auction_offer, - get_public_offer, get_public_offers, get_swap_bid_by_buyer, get_swap_bid_by_seller, - get_swap_bids_by_offer, publish_auction_bid, publish_auction_offers, publish_public_bid, - publish_public_offer, publish_swap_bid, remove_public_offers, update_transfer_bid, - update_transfer_offer, PsbtSwapEx, RgbBid, RgbBidSwap, RgbOffer, RgbOfferErrors, - RgbOfferOptions, RgbOfferSwap, RgbSwapStrategy, + complete_bid, complete_offer, get_auction, get_auction_fifo_bids, get_auction_highest_bids, + get_auction_offer, get_public_offer, get_public_offers, get_swap_bid_by_buyer, + get_swap_bid_by_seller, get_swap_bids_by_offer, publish_auction_bid, + publish_auction_offers, publish_public_bid, publish_public_offer, publish_swap_bid, + remove_public_offers, update_transfer_bid, update_transfer_offer, PsbtSwapEx, + RgbAuctionStrategy, RgbBid, RgbBidSwap, RgbOffer, RgbOfferErrors, RgbOfferOptions, + RgbOfferSwap, RgbSwapStrategy, }, transfer::{extract_transfer, AcceptTransferError, NewInvoiceError, NewPaymentError}, wallet::{ @@ -864,6 +865,8 @@ pub enum RgbSwapError { NoOffer(String), /// Bundle {0} not found. NoBundle(String), + /// The Airdrop strategy needs define fixed bitcoin fee value. + NoAirdropFee, /// The Offer has expired. OfferExpired, /// Insufficient funds (expected: {input} sats / current: {output} sats) @@ -943,8 +946,8 @@ pub async fn create_seller_offer( sk, request, options, - &mut rgb_account, &mut stock, + &mut rgb_account, &mut resolver, ) .await?; @@ -1019,14 +1022,32 @@ pub async fn create_auction_offers( let mut resp = vec![]; let mut collection = vec![]; - let options = RgbOfferOptions::with_bundle_id(sk.to_owned()); - for item in request.offers.clone() { + + let RgbAuctionOfferRequest { + offers, + strategy, + fee, + .. + } = request.clone(); + + let options = match strategy { + RgbAuctionStrategy::Auction => RgbOfferOptions::new(sk.to_owned()), + RgbAuctionStrategy::Airdrop { max_claim } => { + if let Some(fee) = fee { + let contract_amount = ContractAmount::from_decimal_str(max_claim); + RgbOfferOptions::new_airdrop(sk.to_owned(), fee, contract_amount.to_value()) + } else { + return Err(RgbSwapError::NoAirdropFee); + } + } + }; + for item in offers { let mut new_offer = internal_create_seller_offer( sk, item, options.clone(), - &mut rgb_account, &mut stock, + &mut rgb_account, &mut resolver, ) .await?; @@ -1044,7 +1065,7 @@ pub async fn create_auction_offers( .. } = new_offer.clone(); - if !strategy.eq(&RgbSwapStrategy::Auction) { + if ![RgbSwapStrategy::Auction, RgbSwapStrategy::Airdrop].contains(&strategy) { return Err(RgbSwapError::WrongStrategy(strategy.to_string())); } @@ -1086,19 +1107,67 @@ pub async fn create_auction_offers( .await .map_err(RgbSwapError::IO)?; - publish_auction_offers(collection) + publish_auction_offers(request.strategy, collection) .await .map_err(RgbSwapError::Auction)?; Ok(resp) } +pub async fn update_seller_offer( + sk: &str, + request: RgbOfferUpdateRequest, +) -> 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 RgbOfferUpdateRequest { + contract_id, + offer_id, + offer_psbt, + .. + } = request; + + let mut updated = false; + let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + if let Some(offers) = my_offers.offers.get(&contract_id.clone()) { + let mut current_offers = offers.to_owned(); + if let Some(position) = current_offers.iter().position(|x| x.offer_id == offer_id) { + let mut offer = current_offers.swap_remove(position); + offer.seller_psbt = offer_psbt; + current_offers.insert(position, offer.clone()); + my_offers.offers.insert(contract_id.clone(), current_offers); + + updated = true; + store_offers(sk, my_offers) + .await + .map_err(RgbSwapError::IO)?; + + let public_offer = RgbOfferSwap::from(offer); + publish_public_offer(public_offer) + .await + .map_err(RgbSwapError::Marketplace)?; + } + } + + Ok(RgbOfferUpdateResponse { + contract_id, + offer_id, + updated, + }) +} + pub async fn internal_create_seller_offer( sk: &str, request: RgbOfferRequest, options: RgbOfferOptions, - rgb_account: &mut RgbAccountV1, rgb_stock: &mut Stock, + rgb_account: &mut RgbAccountV1, rgb_resolver: &mut ExplorerResolver, ) -> Result { if let Err(err) = request.validate(&RGBContext::default()) { @@ -1137,17 +1206,31 @@ pub async fn internal_create_seller_offer( let boilerplate = export_boilerplate(contr_id, rgb_stock).map_err(|_| RgbSwapError::NoContract)?; + let RgbOfferOptions { + bundle_id, + max_claim, + fee_airdrop, + .. + } = options.clone(); let (allocations, asset_inputs, bitcoin_inputs, mut bitcoin_changes, change_value) = - prebuild_seller_swap(request, rgb_stock, &mut rgb_wallet, rgb_resolver).await?; + prebuild_seller_swap(request, options, rgb_stock, &mut rgb_wallet, rgb_resolver).await?; rgb_account .wallets .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet.clone()); - bitcoin_changes.push(format!("{seller_address}:{bitcoin_price}")); + let (final_fee, psbt_options) = if let Some(fee_airdrop) = fee_airdrop { + (fee_airdrop, NewPsbtOptions::default()) + } else { + bitcoin_changes.push(format!("{seller_address}:{bitcoin_price}")); + ( + PsbtFeeRequest::Value(0), + NewPsbtOptions::set_inflaction(change_value), + ) + }; let psbt_req = PsbtRequest { - fee: PsbtFeeRequest::Value(0), + fee: final_fee, asset_inputs, bitcoin_inputs, bitcoin_changes, @@ -1156,10 +1239,10 @@ pub async fn internal_create_seller_offer( rbf: true, }; - 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(), @@ -1174,60 +1257,13 @@ pub async fn internal_create_seller_offer( change_terminal, strategy, expire_at, - options.bundle_id, + bundle_id, + max_claim, ); Ok(new_offer) } -pub async fn update_seller_offer( - sk: &str, - request: RgbOfferUpdateRequest, -) -> 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 RgbOfferUpdateRequest { - contract_id, - offer_id, - offer_psbt, - .. - } = request; - - let mut updated = false; - let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; - if let Some(offers) = my_offers.offers.get(&contract_id.clone()) { - let mut current_offers = offers.to_owned(); - if let Some(position) = current_offers.iter().position(|x| x.offer_id == offer_id) { - let mut offer = current_offers.swap_remove(position); - offer.seller_psbt = offer_psbt; - current_offers.insert(position, offer.clone()); - my_offers.offers.insert(contract_id.clone(), current_offers); - - updated = true; - store_offers(sk, my_offers) - .await - .map_err(RgbSwapError::IO)?; - - let public_offer = RgbOfferSwap::from(offer); - publish_public_offer(public_offer) - .await - .map_err(RgbSwapError::Marketplace)?; - } - } - - Ok(RgbOfferUpdateResponse { - contract_id, - offer_id, - updated, - }) -} - pub async fn create_buyer_bid( sk: &str, request: RgbBidRequest, @@ -1332,7 +1368,7 @@ pub async fn create_auction_bid( let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; match strategy { - RgbSwapStrategy::Auction => { + RgbSwapStrategy::Auction | RgbSwapStrategy::Airdrop => { let change_terminal = match iface.to_uppercase().as_str() { "RGB20" => "/20/1", "RGB21" => "/21/1", @@ -1369,25 +1405,31 @@ pub async fn create_auction_bid( .await .map_err(RgbSwapError::Transfer)?; - let sign_req = SignPsbtRequest { - psbt: final_psbt, - descriptors: request.sign_keys.clone(), - }; + let buyer_swap_psbt = if let Some(buyer_psbt) = buyer_psbt { + let sign_req = SignPsbtRequest { + psbt: buyer_psbt, + descriptors: request.sign_keys.clone(), + }; - let SignedPsbtResponse { - psbt: final_swap_psbt, - .. - } = sign_psbt_file(sign_req) - .await - .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + let SignedPsbtResponse { + psbt: buyer_swap_psbt, + .. + } = sign_psbt_file(sign_req) + .await + .map_err(|op| RgbSwapError::WrongPsbtBuyer(op.to_string()))?; + + Some(buyer_swap_psbt) + } else { + None + }; let sign_req = SignPsbtRequest { - psbt: buyer_psbt.unwrap_or_default(), + psbt: final_psbt, descriptors: request.sign_keys, }; let SignedPsbtResponse { - psbt: buyer_swap_psbt, + psbt: final_swap_psbt, .. } = sign_psbt_file(sign_req) .await @@ -1412,7 +1454,7 @@ pub async fn create_auction_bid( .await .map_err(RgbSwapError::Auction)?; - bid_swap.buyer_psbt = Some(buyer_swap_psbt.clone()); + bid_swap.buyer_psbt = buyer_swap_psbt; bid_swap.swap_psbt = Some(final_swap_psbt.clone()); publish_auction_bid(bid_swap) .await @@ -1454,12 +1496,16 @@ async fn internal_create_buyer_bid( let RgbBidRequest { offer_id, change_terminal, + asset_amount: bid_amount, .. } = request.clone(); let RgbOfferSwap { iface, + contract_id, bitcoin_price, + asset_precision, + strategy, expire_at, bundle_id, seller_psbt, @@ -1487,77 +1533,104 @@ async fn internal_create_buyer_bid( _ => return Err(RgbSwapError::NoWatcher), }; - let (mut new_bid, bitcoin_inputs, bitcoin_changes, fee_value) = - prebuild_buyer_swap(sk, request, &mut rgb_wallet, resolver).await?; - new_bid.iface = iface.to_uppercase(); + let (mut new_bid, buyer_outpoint, swap_psbt, fee_value) = + if strategy == RgbSwapStrategy::Airdrop { + let buyer_outpoint = watcher_next_utxo(sk, RGB_DEFAULT_NAME, &iface.to_uppercase()) + .await + .map_err(|op| RgbSwapError::NoUtxo(op.to_string()))?; - 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 buyer_outpoint = if let Some(utxo) = buyer_outpoint.utxo { + utxo.outpoint.to_string() + } else { + return Err(RgbSwapError::NoUtxo(String::new())); + }; - if new_bid.bitcoin_amount.cmp(&bitcoin_price) == Ordering::Less { - return Err(RgbSwapError::Inflation { - input: new_bid.bitcoin_amount, - output: bitcoin_price, - }); - }; + let bid_amount = ContractAmount::from_decimal_str(bid_amount); + let new_bid = RgbBid::new( + sk.to_string(), + offer_id, + contract_id.clone(), + bid_amount.to_value(), + asset_precision, + bitcoin_price, + vec![], + ); - let buyer_outpoint = watcher_next_utxo(sk, RGB_DEFAULT_NAME, &iface.to_uppercase()) - .await - .map_err(|op| RgbSwapError::NoUtxo(op.to_string()))?; + (new_bid, buyer_outpoint, seller_psbt, 0) + } else { + let (mut new_bid, bitcoin_inputs, bitcoin_changes, fee_value) = + 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); + } + } - let buyer_outpoint = if let Some(utxo) = buyer_outpoint.utxo { - utxo.outpoint.to_string() - } else { - return Err(RgbSwapError::NoUtxo(String::new())); - }; + if new_bid.bitcoin_amount.cmp(&bitcoin_price) == Ordering::Less { + return Err(RgbSwapError::Inflation { + input: new_bid.bitcoin_amount, + output: bitcoin_price, + }); + }; - rgb_account - .wallets - .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet.clone()); + let buyer_outpoint = watcher_next_utxo(sk, RGB_DEFAULT_NAME, &iface.to_uppercase()) + .await + .map_err(|op| RgbSwapError::NoUtxo(op.to_string()))?; - let psbt_req = PsbtRequest { - fee: PsbtFeeRequest::Value(fee_value), - asset_inputs: vec![], - bitcoin_inputs, - bitcoin_changes, - asset_descriptor_change: None, - asset_terminal_change: Some(change_terminal.clone()), - rbf: true, - }; + let buyer_outpoint = if let Some(utxo) = buyer_outpoint.utxo { + utxo.outpoint.to_string() + } else { + return Err(RgbSwapError::NoUtxo(String::new())); + }; - let options = NewPsbtOptions { - set_tapret: false, - ..default!() - }; + rgb_account + .wallets + .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet.clone()); + + let psbt_req = PsbtRequest { + fee: PsbtFeeRequest::Value(fee_value), + asset_inputs: vec![], + bitcoin_inputs, + bitcoin_changes, + asset_descriptor_change: None, + asset_terminal_change: Some(change_terminal.clone()), + rbf: true, + }; - let PsbtResponse { - psbt: buyer_psbt, .. - } = internal_create_psbt(psbt_req, rgb_account, resolver, Some(options)) - .await - .map_err(RgbSwapError::Create)?; + let options = NewPsbtOptions { + set_tapret: false, + ..default!() + }; + + let PsbtResponse { + psbt: buyer_psbt, .. + } = internal_create_psbt(psbt_req, rgb_account, resolver, Some(options)) + .await + .map_err(RgbSwapError::Create)?; + + new_bid.buyer_psbt = Some(buyer_psbt.clone()); - new_bid.buyer_psbt = Some(buyer_psbt.clone()); + let seller_psbt = Psbt::from_str(&seller_psbt) + .map_err(|op| RgbSwapError::WrongPsbtSeller(op.to_string()))?; - let contract_id = &new_bid.contract_id; - let seller_psbt = - Psbt::from_str(&seller_psbt).map_err(|op| RgbSwapError::WrongPsbtSeller(op.to_string()))?; + let buyer_psbt = Psbt::from_str(&buyer_psbt) + .map_err(|op| RgbSwapError::WrongPsbtBuyer(op.to_string()))?; - let buyer_psbt = - Psbt::from_str(&buyer_psbt).map_err(|op| RgbSwapError::WrongPsbtBuyer(op.to_string()))?; + let seller_psbt = PsbtV0::from(seller_psbt); + let buyer_psbt = PsbtV0::from(buyer_psbt); - let seller_psbt = PsbtV0::from(seller_psbt); - let buyer_psbt = PsbtV0::from(buyer_psbt); + let swap_psbt = seller_psbt + .join(buyer_psbt) + .map_err(|op| RgbSwapError::WrongPsbtSwap(op.to_string()))?; - let swap_psbt = seller_psbt - .join(buyer_psbt) - .map_err(|op| RgbSwapError::WrongPsbtSwap(op.to_string()))?; + let swap_psbt = Psbt::from(swap_psbt); + let swap_psbt = Serialize::serialize(&swap_psbt).to_hex(); - let swap_psbt = Psbt::from(swap_psbt); - let swap_psbt = Serialize::serialize(&swap_psbt).to_hex(); + (new_bid, buyer_outpoint, swap_psbt, fee_value) + }; let RgbBid { bid_id, @@ -1760,100 +1833,6 @@ pub async fn direct_swap_transfer( .await } -async fn internal_transfer_asset( - request: RgbTransferRequest, - options: NewTransferOptions, - stock: &mut Stock, - rgb_account: &mut RgbAccountV1, - rgb_transfers: &mut RgbTransfersV1, -) -> Result { - let network = NETWORK.read().await.to_string(); - let context = RGBContext::with(&network); - - if let Err(err) = request.validate(&context) { - let errors = err - .iter() - .map(|(f, e)| (f.to_string(), e.to_string())) - .collect(); - return Err(TransferError::Validation(errors)); - } - - if rgb_account.wallets.get(RGB_DEFAULT_NAME).is_none() { - return Err(TransferError::NoWatcher); - } - - let RgbTransferRequest { - rgb_invoice: invoice, - psbt, - .. - } = request; - - let (psbt, mut transfers) = - pay_invoice(invoice.clone(), psbt, options.clone(), stock).map_err(TransferError::Pay)?; - let (outpoint, amount, commit) = - extract_output_commit(psbt.clone()).map_err(TransferError::Commitment)?; - - let transfer = transfers.remove(0); - let consig_id = transfer.bindle_id().to_string(); - let consig = transfer - .to_strict_serialized::<{ U32 }>() - .map_err(|err| TransferError::WrongConsig(err.to_string()))?; - - let rgb_invoice = RgbInvoice::from_str(&invoice) - .map_err(|err| TransferError::WrongInvoice(err.to_string()))?; - - let consig = consig.to_hex(); - let commit = commit.to_hex(); - let psbt_hex = psbt.to_string(); - - let iface = rgb_invoice.clone().iface.unwrap().to_string(); - let mut consigs = BTreeMap::default(); - - // TODO: Make strict option check - for prev_invoice in options.other_invoices { - let invoice_contract = prev_invoice.contract.unwrap(); - if let Some(transfer) = transfers - .clone() - .into_iter() - .find(|x| x.contract_id() == invoice_contract) - { - let current_transfer = transfer - .to_strict_serialized::<{ U32 }>() - .map_err(|err| TransferError::WrongConsig(err.to_string()))?; - - let current_transfer = current_transfer.to_hex(); - consigs.insert(prev_invoice.beneficiary.to_string(), current_transfer); - } - } - - let internal_request = RgbInternalSaveTransferRequest::with( - consig_id.clone(), - consig.clone(), - rgb_invoice.beneficiary.to_string(), - iface, - true, - Some(consigs.clone()), - Some(psbt), - ); - - let txid = internal_save_transfer(internal_request, rgb_transfers) - .await - .map_err(TransferError::WrongSave)?; - - let resp = RgbInternalTransferResponse { - consig_id, - consig, - amount, - psbt: psbt_hex, - commit, - outpoint: outpoint.to_string(), - consigs, - txid: txid.to_hex(), - }; - - Ok(resp) -} - pub async fn internal_replace_transfer( sk: &str, request: RgbTransferRequest, @@ -1918,16 +1897,30 @@ pub async fn finish_auction_offers( sk: &str, bundle_id: String, ) -> Result { - let my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; let (mut stock, mut rgb_account, mut rgb_transfers) = retrieve_stock_account_transfers(sk) .await .map_err(RgbSwapError::IO)?; - let offers = my_offers.get_offers(bundle_id.clone()); - let bids = get_auction_highest_bids(bundle_id.clone()) + let my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + let auction = get_auction(&bundle_id.to_string()) .await .map_err(RgbSwapError::Auction)?; + if auction.is_none() || my_offers.clone().get_offers(bundle_id.clone()).is_empty() { + return Err(RgbSwapError::NoBundle(bundle_id)); + } + + let auction = auction.unwrap_or_default(); + let offers = my_offers.get_offers(bundle_id.clone()); + let bids = match auction.strategy { + RgbAuctionStrategy::Auction => get_auction_highest_bids(bundle_id.clone()) + .await + .map_err(RgbSwapError::Auction)?, + RgbAuctionStrategy::Airdrop { max_claim: _ } => get_auction_fifo_bids(bundle_id.clone()) + .await + .map_err(RgbSwapError::Auction)?, + }; + let mut final_psbt: Option = none!(); for bid in bids.clone() { if let Some(offer) = offers @@ -1938,15 +1931,19 @@ pub async fn finish_auction_offers( let seller_part = Psbt::from_str(&offer.seller_psbt) .map_err(|op| RgbSwapError::WrongPsbtSeller(op.to_string()))?; - let buyer_part = Psbt::from_str(&bid.buyer_psbt.unwrap_or_default()) - .map_err(|op| RgbSwapError::WrongPsbtBuyer(op.to_string()))?; + let swap_part = if let Some(buyer_psbt) = bid.buyer_psbt { + let buyer_part = Psbt::from_str(&buyer_psbt) + .map_err(|op| RgbSwapError::WrongPsbtBuyer(op.to_string()))?; - let seller_part = PsbtV0::from(seller_part); - let buyer_part = PsbtV0::from(buyer_part); + let seller_part = PsbtV0::from(seller_part); + let buyer_part = PsbtV0::from(buyer_part); - let swap_part = seller_part - .join(buyer_part) - .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + seller_part + .join(buyer_part) + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))? + } else { + PsbtV0::from(seller_part) + }; final_psbt = if let Some(final_psbt) = final_psbt { let final_psbt = final_psbt @@ -2127,6 +2124,100 @@ pub async fn finish_auction_offers( Ok(resp) } +async fn internal_transfer_asset( + request: RgbTransferRequest, + options: NewTransferOptions, + stock: &mut Stock, + rgb_account: &mut RgbAccountV1, + rgb_transfers: &mut RgbTransfersV1, +) -> Result { + let network = NETWORK.read().await.to_string(); + let context = RGBContext::with(&network); + + if let Err(err) = request.validate(&context) { + let errors = err + .iter() + .map(|(f, e)| (f.to_string(), e.to_string())) + .collect(); + return Err(TransferError::Validation(errors)); + } + + if rgb_account.wallets.get(RGB_DEFAULT_NAME).is_none() { + return Err(TransferError::NoWatcher); + } + + let RgbTransferRequest { + rgb_invoice: invoice, + psbt, + .. + } = request; + + let (psbt, mut transfers) = + pay_invoice(invoice.clone(), psbt, options.clone(), stock).map_err(TransferError::Pay)?; + let (outpoint, amount, commit) = + extract_output_commit(psbt.clone()).map_err(TransferError::Commitment)?; + + let transfer = transfers.remove(0); + let consig_id = transfer.bindle_id().to_string(); + let consig = transfer + .to_strict_serialized::<{ U32 }>() + .map_err(|err| TransferError::WrongConsig(err.to_string()))?; + + let rgb_invoice = RgbInvoice::from_str(&invoice) + .map_err(|err| TransferError::WrongInvoice(err.to_string()))?; + + let consig = consig.to_hex(); + let commit = commit.to_hex(); + let psbt_hex = psbt.to_string(); + + let iface = rgb_invoice.clone().iface.unwrap().to_string(); + let mut consigs = BTreeMap::default(); + + // TODO: Make strict option check + for prev_invoice in options.other_invoices { + let invoice_contract = prev_invoice.contract.unwrap(); + if let Some(transfer) = transfers + .clone() + .into_iter() + .find(|x| x.contract_id() == invoice_contract) + { + let current_transfer = transfer + .to_strict_serialized::<{ U32 }>() + .map_err(|err| TransferError::WrongConsig(err.to_string()))?; + + let current_transfer = current_transfer.to_hex(); + consigs.insert(prev_invoice.beneficiary.to_string(), current_transfer); + } + } + + let internal_request = RgbInternalSaveTransferRequest::with( + consig_id.clone(), + consig.clone(), + rgb_invoice.beneficiary.to_string(), + iface, + true, + Some(consigs.clone()), + Some(psbt), + ); + + let txid = internal_save_transfer(internal_request, rgb_transfers) + .await + .map_err(TransferError::WrongSave)?; + + let resp = RgbInternalTransferResponse { + consig_id, + consig, + amount, + psbt: psbt_hex, + commit, + outpoint: outpoint.to_string(), + consigs, + txid: txid.to_hex(), + }; + + Ok(resp) +} + pub async fn list_auctions() -> Result, RgbSwapError> { let utc = chrono::Local::now().naive_utc().timestamp(); let auction_offers: Vec<_> = get_public_offers() diff --git a/src/rgb/prebuild.rs b/src/rgb/prebuild.rs index ae000ae9..722ea11c 100644 --- a/src/rgb/prebuild.rs +++ b/src/rgb/prebuild.rs @@ -44,6 +44,8 @@ use crate::rgb::{ RgbSwapError, SaveTransferError, TransferError, }; +use super::swap::RgbOfferOptions; + pub const DUST_LIMIT_SATOSHI: u64 = 546; pub async fn prebuild_transfer_asset( @@ -386,6 +388,7 @@ pub async fn prebuild_transfer_asset( pub async fn prebuild_seller_swap( request: RgbOfferRequest, + options: RgbOfferOptions, stock: &mut Stock, rgb_wallet: &mut RgbWallet, resolver: &mut ExplorerResolver, @@ -513,11 +516,7 @@ pub async fn prebuild_seller_swap( let mut total_asset_bitcoin_unspend: u64 = 0; - let asset_sig_hash = match strategy { - RgbSwapStrategy::Auction => PsbtSigHashRequest::NonePlusAnyoneCanPay, - RgbSwapStrategy::P2P | RgbSwapStrategy::HotSwap => PsbtSigHashRequest::NonePlusAnyoneCanPay, - }; - + let asset_sig_hash = PsbtSigHashRequest::NonePlusAnyoneCanPay; for alloc in allocations.iter() { match alloc.value { AllocationValue::Value(alloc_value) => { @@ -584,7 +583,7 @@ pub async fn prebuild_seller_swap( } let (bitcoin_inputs, change_value) = match strategy { - RgbSwapStrategy::P2P | RgbSwapStrategy::HotSwap => { + RgbSwapStrategy::P2P | RgbSwapStrategy::HotSwap | RgbSwapStrategy::Airdrop => { // Get All Bitcoin UTXOs let total_bitcoin_spend: u64 = bitcoin_changes .clone() @@ -619,7 +618,16 @@ pub async fn prebuild_seller_swap( } let mut bitcoin_total = total_asset_bitcoin_unspend; - let total_spendable = total_bitcoin_spend; + let total_spendable = match (strategy, options.fee_airdrop) { + (RgbSwapStrategy::Airdrop, Some(fee)) => match fee { + PsbtFeeRequest::Value(fee_value) => { + total_bitcoin_spend + fee_value + DUST_LIMIT_SATOSHI + } + PsbtFeeRequest::FeeRate(_) => return Err(RgbSwapError::NoAirdropFee), + }, + (RgbSwapStrategy::Airdrop, None) => return Err(RgbSwapError::NoAirdropFee), + _ => total_bitcoin_spend, + }; let bitcoin_sigh_hash = PsbtSigHashRequest::NonePlusAnyoneCanPay; for utxo in all_unspents { diff --git a/src/rgb/swap.rs b/src/rgb/swap.rs index e13f56f4..7b4c9880 100644 --- a/src/rgb/swap.rs +++ b/src/rgb/swap.rs @@ -28,13 +28,16 @@ use strict_encoding::{ StrictDecode, StrictDeserialize, StrictDumb, StrictEncode, StrictSerialize, StrictType, }; -use crate::rgb::{ - constants::LIB_NAME_BITMASK, - crdt::{LocalRgbAuctions, LocalRgbOfferBid, LocalRgbOffers}, - fs::{ - retrieve_auctions_offers, retrieve_public_offers, retrieve_swap_offer_bid, - store_auction_offers, store_public_offers, store_swap_bids, RgbPersistenceError, +use crate::{ + rgb::{ + constants::LIB_NAME_BITMASK, + crdt::{LocalRgbAuctions, LocalRgbOfferBid, LocalRgbOffers}, + fs::{ + retrieve_auctions_offers, retrieve_public_offers, retrieve_swap_offer_bid, + store_auction_offers, store_public_offers, store_swap_bids, RgbPersistenceError, + }, }, + structs::PsbtFeeRequest, }; use crate::{structs::AllocationDetail, validators::RGBContext}; @@ -79,22 +82,56 @@ pub enum RgbSwapStrategy { P2P, #[serde(rename = "hotswap")] HotSwap, + #[serde(rename = "airdrop")] + Airdrop, +} + +#[derive( + Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Default, Reconcile, Hydrate, Display, +)] +#[serde(rename_all = "camelCase")] +#[display(inner)] +pub enum RgbAuctionStrategy { + #[default] + #[serde(rename = "auction")] + Auction, + #[serde(rename = "airdrop")] + Airdrop { max_claim: String }, } #[derive(Clone, Debug, Display, Default, Error)] #[display(doc_comments)] pub struct RgbOfferOptions { pub bundle_id: Option, + pub max_claim: Option, + pub fee_airdrop: Option, } impl RgbOfferOptions { - pub fn with_bundle_id(secret: String) -> Self { + pub fn new(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, + fee_airdrop: None, + max_claim: None, + } + } + + pub fn new_airdrop(secret: String, fee: PsbtFeeRequest, max: u64) -> 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 } + Self { + bundle_id, + fee_airdrop: Some(fee), + max_claim: Some(max), + } } } @@ -132,6 +169,8 @@ pub struct RgbOffer { #[garde(skip)] pub bundle_id: Option, #[garde(skip)] + pub max_claim: Option, + #[garde(skip)] pub transfer_id: Option, } @@ -155,6 +194,7 @@ impl RgbOffer { strategy: RgbSwapStrategy, expire_at: Option, bundle_id: Option, + max_claim: Option, ) -> Self { let secp = Secp256k1::new(); let secret = hex::decode(secret).expect("cannot decode hex sk in new RgbOffer"); @@ -189,6 +229,7 @@ impl RgbOffer { terminal, strategy, bundle_id, + max_claim, ..Default::default() } } @@ -222,6 +263,8 @@ pub struct RgbOfferSwap { #[garde(skip)] pub bundle_id: Option, #[garde(skip)] + pub max_claim: Option, + #[garde(skip)] pub expire_at: Option, } @@ -240,6 +283,7 @@ impl From for RgbOfferSwap { strategy, expire_at, bundle_id, + max_claim, .. } = value; @@ -262,6 +306,7 @@ impl From for RgbOfferSwap { pub_key, bundle_id, expire_at, + max_claim, } } } @@ -598,6 +643,7 @@ impl RgbPublicSwaps { #[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] pub struct RgbAuctionSwaps { pub bundle_id: String, + pub strategy: RgbAuctionStrategy, pub items: Vec, pub bids: BTreeMap>, } @@ -698,6 +744,16 @@ 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)?; + + Ok(Some(rgb_offers)) +} + pub async fn get_auction_offer( bundle_id: &str, offer_id: OfferId, @@ -830,6 +886,24 @@ pub async fn get_swap_bid_by_buyer( Ok(rgb_bid) } +pub async fn get_auction_fifo_bids(bundle_id: String) -> 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)?; + + let mut first_bids = vec![]; + for RgbOfferSwap { offer_id, .. } in rgb_offers.clone().get_offers(bundle_id) { + if let Some(bids) = rgb_offers.bids.get(&offer_id).cloned() { + if let Some(bid) = bids.first() { + first_bids.push(bid.clone()); + } + }; + } + + Ok(first_bids) +} + pub async fn get_auction_highest_bids( bundle_id: String, ) -> Result, RgbOfferErrors> { @@ -984,7 +1058,10 @@ pub async fn publish_swap_bid( Ok(()) } -pub async fn publish_auction_offers(new_offers: Vec) -> Result<(), RgbOfferErrors> { +pub async fn publish_auction_offers( + strategy: RgbAuctionStrategy, + 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}"); @@ -1000,6 +1077,8 @@ pub async fn publish_auction_offers(new_offers: Vec) -> Result<(), .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; rgb_offers = rgb_offers.save_offers(new_offers.clone()); + rgb_offers.strategy = strategy; + reconcile(&mut current_version, rgb_offers.clone()) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; diff --git a/src/structs.rs b/src/structs.rs index 9aa6cd0d..82851338 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, RgbSwapStrategy}, + swap::{PublicRgbBid, RgbAuctionStrategy, RgbBid, RgbOffer, RgbOfferSwap, RgbSwapStrategy}, }, validators::{ verify_descriptor, verify_media_request, verify_rgb_invoice, verify_tapret_seal, @@ -1349,12 +1349,17 @@ impl UtxoSpentStatus { #[serde(rename_all = "camelCase")] #[display(doc_comments)] pub struct RgbAuctionOfferRequest { + #[garde(dive)] + pub offers: Vec, + #[garde(skip)] pub sign_keys: Vec, - /// List of Offers + #[garde(skip)] + pub strategy: RgbAuctionStrategy, + #[garde(dive)] - pub offers: Vec, + pub fee: Option, } #[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] diff --git a/src/web.rs b/src/web.rs index 1b545c90..03c1274c 100644 --- a/src/web.rs +++ b/src/web.rs @@ -9,9 +9,9 @@ use crate::rgb::structs::ContractAmount; use crate::structs::{ AcceptRequest, FullIssueRequest, FullRgbTransferRequest, ImportRequest, InvoiceRequest, IssueMediaRequest, IssueRequest, MediaRequest, PsbtRequest, PublishPsbtRequest, ReIssueRequest, - RgbAuctionBidRequest, RgbBidRequest, RgbOfferRequest, RgbRemoveTransferRequest, - RgbSaveTransferRequest, RgbSwapRequest, RgbTransferRequest, SecretString, SignPsbtRequest, - WatcherRequest, + RgbAuctionBidRequest, RgbAuctionOfferRequest, RgbBidRequest, RgbOfferRequest, + RgbRemoveTransferRequest, RgbSaveTransferRequest, RgbSwapRequest, RgbTransferRequest, + SecretString, SignPsbtRequest, WatcherRequest, }; pub fn set_panic_hook() { @@ -860,6 +860,7 @@ pub mod rgb { }) } + // P2P & Hotswap #[wasm_bindgen] pub fn create_offer(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); @@ -876,12 +877,12 @@ pub mod rgb { } #[wasm_bindgen] - pub fn create_auction_bid(nostr_hex_sk: String, request: JsValue) -> Promise { + pub fn create_bid(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); future_to_promise(async move { - let bid_req: RgbAuctionBidRequest = serde_wasm_bindgen::from_value(request).unwrap(); - match crate::rgb::create_auction_bid(&nostr_hex_sk, bid_req).await { + let bid_req: RgbBidRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_buyer_bid(&nostr_hex_sk, bid_req).await { Ok(result) => Ok(JsValue::from_string( serde_json::to_string(&result).unwrap(), )), @@ -891,12 +892,12 @@ pub mod rgb { } #[wasm_bindgen] - pub fn create_bid(nostr_hex_sk: String, request: JsValue) -> Promise { + pub fn create_swap(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); future_to_promise(async move { - let bid_req: RgbBidRequest = serde_wasm_bindgen::from_value(request).unwrap(); - match crate::rgb::create_buyer_bid(&nostr_hex_sk, bid_req).await { + let swap_req: RgbSwapRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_swap_transfer(&nostr_hex_sk, swap_req).await { Ok(result) => Ok(JsValue::from_string( serde_json::to_string(&result).unwrap(), )), @@ -905,13 +906,30 @@ pub mod rgb { }) } + // Auctions & Airdrop #[wasm_bindgen] - pub fn create_swap(nostr_hex_sk: String, request: JsValue) -> Promise { + pub fn create_auction_offers(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); future_to_promise(async move { - let swap_req: RgbSwapRequest = serde_wasm_bindgen::from_value(request).unwrap(); - match crate::rgb::create_swap_transfer(&nostr_hex_sk, swap_req).await { + let offers_req: RgbAuctionOfferRequest = + serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_auction_offers(&nostr_hex_sk, offers_req).await { + Ok(result) => Ok(JsValue::from_string( + serde_json::to_string(&result).unwrap(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } + + #[wasm_bindgen] + pub fn create_auction_bid(nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let bid_req: RgbAuctionBidRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_auction_bid(&nostr_hex_sk, bid_req).await { Ok(result) => Ok(JsValue::from_string( serde_json::to_string(&result).unwrap(), )), @@ -921,7 +939,7 @@ pub mod rgb { } #[wasm_bindgen] - pub fn finish_auction(nostr_hex_sk: String, request: JsValue) -> Promise { + pub fn finish_auction_offers(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); future_to_promise(async move { diff --git a/tests/rgb/integration/swaps.rs b/tests/rgb/integration/swaps.rs index 735fd929..dea6eeb5 100644 --- a/tests/rgb/integration/swaps.rs +++ b/tests/rgb/integration/swaps.rs @@ -11,7 +11,9 @@ use bitmask_core::{ rgb::{ accept_transfer, create_auction_bid, create_auction_offers, create_buyer_bid, create_seller_offer, create_swap_transfer, create_watcher, finish_auction_offers, - get_contract, import as import_contract, structs::ContractAmount, swap::RgbSwapStrategy, + get_contract, import as import_contract, + structs::ContractAmount, + swap::{RgbAuctionStrategy, RgbSwapStrategy}, update_seller_offer, verify_transfers, }, structs::{ @@ -850,12 +852,14 @@ async fn create_auction_swap() -> anyhow::Result<()> { } let offer_auction_req = RgbAuctionOfferRequest { + strategy: RgbAuctionStrategy::Auction, offers: offers_collection.clone(), sign_keys: vec![ SecretString(seller_keys.private.btc_descriptor_xprv.clone()), SecretString(seller_keys.private.btc_change_descriptor_xprv.clone()), SecretString(seller_keys.private.rgb_assets_descriptor_xprv.clone()), ], + ..Default::default() }; let resp = create_auction_offers(&seller_sk, offer_auction_req).await; @@ -1082,6 +1086,7 @@ async fn create_collectible_auction() -> anyhow::Result<()> { SecretString(alice_keys.private.btc_change_descriptor_xprv.clone()), SecretString(alice_keys.private.rgb_udas_descriptor_xprv.clone()), ], + ..Default::default() }; let resp = create_auction_offers(&alice_sk, offer_auction_req).await; @@ -1169,3 +1174,199 @@ async fn create_collectible_auction() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn create_airdrop_swap() -> anyhow::Result<()> { + init_logging("bitmask_core=debug"); + + // 1. Initial Setup + let seller_keys = new_mnemonic(&SecretString("".to_string())).await?; + let buyer_keys = new_mnemonic(&SecretString("".to_string())).await?; + + let watcher_name = "default"; + let seller_sk = seller_keys.private.nostr_prv.clone(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: seller_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&seller_sk, create_watch_req.clone()).await?; + + let buyer_sk = buyer_keys.private.nostr_prv.clone(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: buyer_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&buyer_sk, create_watch_req.clone()).await?; + + // 2. Setup Wallets (Seller) + let btc_address_1 = get_new_address( + &SecretString(seller_keys.public.btc_descriptor_xpub.clone()), + None, + ) + .await?; + + let default_coins = "0.001"; + send_some_coins(&btc_address_1, default_coins).await; + + let btc_descriptor_xprv = SecretString(seller_keys.private.btc_descriptor_xprv.clone()); + let btc_change_descriptor_xprv = + SecretString(seller_keys.private.btc_change_descriptor_xprv.clone()); + + let assets_address_1 = get_new_address( + &SecretString(seller_keys.public.rgb_assets_descriptor_xpub.clone()), + None, + ) + .await?; + + let uda_address_1 = get_new_address( + &SecretString(seller_keys.public.rgb_udas_descriptor_xpub.clone()), + None, + ) + .await?; + + let btc_wallet = get_wallet(&btc_descriptor_xprv, Some(&btc_change_descriptor_xprv)).await?; + sync_wallet(&btc_wallet).await?; + + let fund_vault = fund_vault( + &btc_descriptor_xprv, + &btc_change_descriptor_xprv, + &assets_address_1, + &uda_address_1, + Some(1.1), + ) + .await?; + + // 3. Send some coins (Buyer) + let btc_address_1 = get_new_address( + &SecretString(buyer_keys.public.btc_descriptor_xpub.clone()), + None, + ) + .await?; + let asset_address_1 = get_new_address( + &SecretString(buyer_keys.public.rgb_assets_descriptor_xpub.clone()), + None, + ) + .await?; + + let default_coins = "0.1"; + send_some_coins(&btc_address_1, default_coins).await; + send_some_coins(&asset_address_1, default_coins).await; + + // 4. Issue Contract (Seller) + let issuer_resp = issuer_issue_contract_v2( + 1, + "RGB20", + ContractAmount::with(1000, 0, 2).to_value(), + false, + false, + None, + None, + Some(UtxoFilter::with_outpoint( + fund_vault.assets_output.unwrap_or_default(), + )), + Some(seller_keys.clone()), + ) + .await?; + + for contract in issuer_resp.clone() { + let buyer_import_req = ImportRequest { + import: AssetType::RGB20, + data: contract.contract.strict, + }; + let buyer_import_resp = import_contract(&buyer_sk, buyer_import_req).await; + assert!(buyer_import_resp.is_ok()); + } + + // 5. Create Collection (Seller) + let contract_amount = "500.00".to_string(); + let mut offers_collection = vec![]; + for contract in issuer_resp.clone() { + let IssueResponse { + contract_id, iface, .. + } = contract.clone(); + + let desc = SecretString(seller_keys.public.rgb_assets_descriptor_xpub.clone()); + let req = RgbOfferRequest { + contract_id, + iface, + contract_amount: contract_amount.clone(), + bitcoin_price: 0, + descriptor: desc, + change_terminal: "/20/1".to_string(), + bitcoin_changes: vec![], + strategy: RgbSwapStrategy::Airdrop, + expire_at: None, + }; + + offers_collection.push(req); + } + + let offer_auction_req = RgbAuctionOfferRequest { + strategy: RgbAuctionStrategy::Airdrop { + max_claim: "1000".to_string(), + }, + fee: Some(PsbtFeeRequest::Value(1000)), + offers: offers_collection.clone(), + sign_keys: vec![ + SecretString(seller_keys.private.btc_descriptor_xprv.clone()), + SecretString(seller_keys.private.btc_change_descriptor_xprv.clone()), + SecretString(seller_keys.private.rgb_assets_descriptor_xprv.clone()), + ], + }; + + let resp = create_auction_offers(&seller_sk, offer_auction_req).await; + assert!(resp.is_ok()); + + let mut offers = resp?; + let RgbOfferResponse { + offer_id: offer_1st, + contract_id: contract_1st, + bundle_id, + .. + } = offers.remove(0); + + // 6. Create Bid (1st Offer) + let buyer_btc_desc = buyer_keys.public.btc_descriptor_xpub.clone(); + let bid_auction_req = RgbAuctionBidRequest { + offer_id: offer_1st.clone(), + asset_amount: contract_amount.clone(), + descriptor: SecretString(buyer_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(0), + sign_keys: vec![ + SecretString(buyer_keys.private.btc_descriptor_xprv.clone()), + SecretString(buyer_keys.private.btc_change_descriptor_xprv.clone()), + ], + }; + + let resp = create_auction_bid(&buyer_sk, bid_auction_req).await; + + assert!(resp.is_ok()); + + // 7. Finish Offer + let resp = finish_auction_offers(&seller_sk, bundle_id.unwrap_or_default()).await; + assert!(resp.is_ok()); + + // 8. Mine Some Blocks + generate_new_block().await; + + // 10. Verify Transfers + let all_sks = [seller_sk.clone(), buyer_sk.clone()]; + for sk in all_sks { + let resp = verify_transfers(&sk).await; + assert!(resp.is_ok()); + } + + // 11. Check Balances (1st Offer) + let resp = get_contract(&buyer_sk, &contract_1st).await; + assert!(resp.is_ok()); + assert_eq!(500., resp?.balance_normalized); + + let resp = get_contract(&seller_sk, &contract_1st).await; + assert!(resp.is_ok()); + assert_eq!(500., resp?.balance_normalized); + + Ok(()) +}