diff --git a/.env b/.env index 03fd6e8d..358fb20f 100644 --- a/.env +++ b/.env @@ -32,11 +32,13 @@ CARBONADO_ENDPOINT=http://localhost:7070/carbonado UDAS_UTXO=3b367e1facc3174e97658295961faf6a4ed889129c881b7a73db1f074b49bd8a: MARKETPLACE_SEED="lion bronze dumb tuna perfect fantasy wall orphan improve business harbor sadness" MARKETPLACE_NOSTR=cd591c134a0d88991326b1619953d0eae2287d315a7c4a93c1e4883a8c26c464 - # 1..100 MARKETPLACE_FEE_PERC= # xpub.. MARKETPLACE_FEE_XPUB= +# :: Coordinator :: +COORDINATOR_NOSTR=9e8294eb38ba77c0fba982da8fbd370b8868c6dbfc9ca414aff4863c15dfbcff + # :: RGB PROXY :: RGB_PROXY_ENDPOINT=http://localhost:3001 diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 004a3c71..3a032edb 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -106,8 +106,11 @@ jobs: MAIN_VAULT_ADDRESS: ${{ secrets.MAIN_VAULT_ADDRESS }} RUST_BACKTRACE: 1 + - name: RGB Test Init + run: cargo test --locked --features server --test _init -- _init --nocapture --test-threads 1 + - name: RGB Tests - run: cargo test --locked --features server --test rgb -- rgb --nocapture --test-threads 1 + run: cargo test --locked --features server --test rgb -- rgb --nocapture --test-threads 1 env: TEST_WALLET_SEED: ${{ secrets.TEST_WALLET_SEED }} RUST_BACKTRACE: 1 diff --git a/Cargo.lock b/Cargo.lock index 4da41365..f9597f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,7 @@ dependencies = [ "serde_json", "serde_yaml", "stringly_conversions", - "toml 0.8.8", + "toml", "wasm-bindgen", ] @@ -719,11 +719,12 @@ dependencies = [ "serde-encrypt", "serde-wasm-bindgen", "serde_json", + "sled", "strict_encoding", "strict_types", "thiserror", "tokio", - "toml 0.7.8", + "toml", "tower-http", "walkdir", "wasm-bindgen", @@ -1114,6 +1115,27 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" +[[package]] +name = "crossbeam-epoch" +version = "0.9.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +dependencies = [ + "cfg-if", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1301,7 +1323,7 @@ dependencies = [ "hkdf", "libsecp256k1", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "rand_core 0.6.4", "sha2 0.10.8", "typenum", @@ -1437,6 +1459,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.29" @@ -2183,6 +2215,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2418,6 +2459,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2425,7 +2477,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -2436,7 +2502,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -2684,6 +2750,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3380,6 +3455,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + [[package]] name = "slip132" version = "0.10.1" @@ -3515,7 +3606,7 @@ dependencies = [ "serde_yaml", "sha2 0.10.8", "strict_encoding", - "toml 0.8.8", + "toml", ] [[package]] @@ -3597,7 +3688,7 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.4.1", "rustix", "windows-sys 0.48.0", ] @@ -3686,7 +3777,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", @@ -3766,29 +3857,17 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" -dependencies = [ - "indexmap 2.1.0", - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", -] - [[package]] name = "toml" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.0", + "toml_edit", ] [[package]] @@ -3800,19 +3879,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.1.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "toml_edit" version = "0.21.0" diff --git a/Cargo.toml b/Cargo.toml index 6d0905c7..e532d38a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ esplora_block = { version = "0.5.0", package = "esplora-client", default-feature "blocking", ] } inflate = "0.4.5" +sled = "0.34.7" tower-http = { version = "0.4.4", features = ["cors"], optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] @@ -134,7 +135,8 @@ anyhow = "1.0.71" blake3 = "1.4.1" rgb-std = { version = "0.10.2" } serde = "1.0.189" -toml = { version = "0.7.8", features = ["preserve_order"] } +serde_json = "1.0.107" +toml = { version = "0.8.0", features = ["preserve_order"] } [patch.crates-io] # Remove after merge and release https://github.com/BP-WG/bitcoin_foundation/pull/20 diff --git a/build.rs b/build.rs index 5b5cfa4e..5b61dad4 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, fs}; +use std::{collections::BTreeMap, env, fs, path}; use anyhow::Result; use rgbstd::{ @@ -54,6 +54,30 @@ const MARKETPLACE_OFFERS: &str = "bitmask-marketplace_public_offers.c15"; const MARKETPLACE_BIDS: &str = "bitmask-marketplace_public_bids.c15"; const NETWORK: &str = "bitcoin"; // Only mainnet is tracked, no monetary incentive to upgrade testnet assets +#[derive(Serialize, Deserialize, Default)] +pub struct MetricsData { + bytes: u64, + bytes_by_day: BTreeMap, + bitcoin_wallets_by_day: BTreeMap, + signet_wallets_by_day: BTreeMap, + testnet_wallets_by_day: BTreeMap, + regtest_wallets_by_day: BTreeMap, + wallets_by_network: BTreeMap, +} + +pub fn init_fs() -> Result<()> { + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + let dir = path::Path::new(&dir); + + fs::create_dir_all(dir)?; + fs::write( + dir.join("metrics.json"), + serde_json::to_string_pretty(&MetricsData::default())?, + )?; + + Ok(()) +} + fn main() -> Result<()> { // lib ids const BMC_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -158,5 +182,7 @@ fn main() -> Result<()> { fs::write(FILE_HASHES_FILE, toml)?; + init_fs()?; + Ok(()) } diff --git a/lib/web/bitcoin.ts b/lib/web/bitcoin.ts index cb98ff83..931e176e 100644 --- a/lib/web/bitcoin.ts +++ b/lib/web/bitcoin.ts @@ -173,6 +173,12 @@ export interface TransactionDetails extends Transaction { } export interface TransactionData { + details: TransactionDataDetails; + vsize: number; + feeRate: number; +} + +export interface TransactionDataDetails { transaction?: Transaction; txid: string; received: number; diff --git a/lib/web/package-lock.json b/lib/web/package-lock.json index 88fa2a3f..bcacd74c 100644 --- a/lib/web/package-lock.json +++ b/lib/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitmask-core", - "version": "0.7.0-beta.8", + "version": "0.7.0-beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bitmask-core", - "version": "0.7.0-beta.8", + "version": "0.7.0-beta.10", "license": "MIT", "devDependencies": { "@types/node": "^20.8.2", diff --git a/lib/web/package.json b/lib/web/package.json index fecd44f0..d6d90191 100644 --- a/lib/web/package.json +++ b/lib/web/package.json @@ -6,7 +6,7 @@ "Francisco Calderón " ], "description": "Core functionality for the BitMask wallet", - "version": "0.7.0-beta.8", + "version": "0.7.0-beta.10", "license": "MIT", "repository": { "type": "git", diff --git a/lib/web/rgb.ts b/lib/web/rgb.ts index 71f5189f..d8bb10d0 100644 --- a/lib/web/rgb.ts +++ b/lib/web/rgb.ts @@ -157,6 +157,12 @@ 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 @@ -169,6 +175,15 @@ export const createSwap = async ( ): Promise => JSON.parse(await BMC.create_swap(nostrHexSk, request)); +export const finishAuction = async ( + nostrHexSk: string, + request: string +): Promise => + JSON.parse(await BMC.finish_auction(nostrHexSk, request)); + +export const listAuctions = async (): Promise => + JSON.parse(await BMC.list_auctions()); + export const directSwap = async ( nostrHexSk: string, request: RgbBidRequest @@ -432,15 +447,15 @@ export interface InvoiceResponse { export interface PsbtRequest { /// Asset UTXOs - asset_inputs: PsbtInputRequest[]; + assetInputs: PsbtInputRequest[]; /// Asset Descriptor Change - asset_descriptor_change: string; + assetDescriptorChange: string; /// Asset Terminal Change (default: /10/0) - asset_terminal_change: string; + assetTerminalChange: string; /// Bitcoin UTXOs - bitcoin_inputs: PsbtInputRequest[]; + bitcoinInputs: PsbtInputRequest[]; /// Bitcoin Change Addresses (format: {address}:{amount}) - bitcoin_changes: string[]; + bitcoinChanges: string[]; /// Bitcoin Fee fee: PsbtFeeRequest; /// Allow RBF @@ -453,11 +468,11 @@ interface PsbtInputRequest { /// Asset or Bitcoin UTXO utxo: string; /// Asset or Bitcoin UTXO Terminal (ex. /0/0) - utxo_terminal: string; + utxoTerminal: string; /// Asset or Bitcoin Tweak tapret?: string; /// Asset or Bitcoin Tweak - sigh_hash?: PsbtSigHashRequest; + sighHash?: PsbtSigHashRequest; } interface PsbtSigHashRequest { @@ -748,7 +763,7 @@ export interface RgbTransferDetail { } export interface TxStatus { - not_found?: any; + notFound?: any; error?: string; mempool?: any; block?: number; @@ -793,8 +808,66 @@ export interface RgbOfferRequest { changeTerminal: string; /// Bitcoin Change Addresses (format: {address}:{amount}) bitcoinChanges: string[]; - presig: boolean; - expire_at?: number; + strategy: RgbSwapStrategy; + expireAt?: number; +} + +export interface RgbSwapStrategy { + auction?: string, + p2p?: string, + hotswap?: string, +} +export interface RgbAuctionOfferRequest { + signKeys: string[], + + /// List of Offers + offers: RgbOfferRequest[], +} + +export interface RgbAuctionBidRequest { + /// The Offer ID + offerId: string, + /// Asset Amount + assetAmount: string, + /// Universal Descriptor + descriptor: string, + /// Bitcoin Terminal Change + changeTerminal: string, + /// Descriptors to Sign + signKeys: string[], + /// Bitcoin Fee + fee: PsbtFeeRequest, +} + +export interface RgbAuctionBidResponse { + /// The Bid ID + bidId: string, + /// The Offer ID + offerId: string, + /// Fee Value + feeValue: number, +} + +export interface RgbMatchResponse { + /// Transfer ID + consigId: string, + /// Offer ID + offerId: string, + /// Bid ID + bidId: string, +} + +export interface RgbAuctionOfferResponse { + /// Offer ID + offerId: string, + /// Contract ID + contractId: string, + /// Asset/Contract Amount + assetAmount: number, + /// Bitcoin Price + bitcoinPrice: number, + /// Bundle ID + bundleId: string, } export interface RgbOfferResponse { @@ -810,6 +883,8 @@ export interface RgbOfferResponse { sellerAddress: string; /// Seller PSBT (encoded in base64) sellerPsbt: string; + /// Bundle ID (collection) + bundleId?: string, } export interface RgbBidRequest { @@ -875,9 +950,30 @@ export interface PublicRgbOfferResponse { /// Bitcoin Price bitcoinPrice: bigint; /// Initial Offer PSBT - offerPsbt: string; + offerPsbt?: string; } +export interface RgbAuctionFinishResponse { + /// Bundle ID + bundle_id: string, + /// New Change Outpoint + outpoint: string, + /// Sold Items + sold: Map, + /// Reamining Items + remaining: Map, +} + +export interface RgbSwapItem { + /// Contract ID + contractId: string, + /// Iface + iface: string, + /// Final Consig + contractAmount: string, +} + + export interface PublicRgbBidResponse { /// Bid ID bidId: string; diff --git a/src/bin/bitmaskd.rs b/src/bin/bitmaskd.rs index 658ce315..fb65e803 100644 --- a/src/bin/bitmaskd.rs +++ b/src/bin/bitmaskd.rs @@ -25,7 +25,7 @@ use axum::{ use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, SecretKey}; use bitmask_core::{ bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, - carbonado::{handle_file, metrics, server_retrieve, server_store, store}, + carbonado::{handle_file, marketplace_retrieve, marketplace_store, metrics, store}, constants::{ get_marketplace_nostr_key, get_marketplace_seed, get_network, get_udas_utxo, switch_network, }, @@ -474,7 +474,7 @@ async fn co_store( }, } - metrics::update(&filepath).await?; + // metrics::update(&filepath).await?; Ok((StatusCode::OK, TypedHeader(cc), "Success")) } @@ -527,7 +527,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) @@ -627,7 +627,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 { @@ -687,6 +687,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 { @@ -724,7 +774,8 @@ async fn send_coins( } async fn json_metrics() -> Result { - let metrics_json = metrics::json().await?; + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + let metrics_json = fs::read_to_string(&format!("{dir}/metrics.json")).await?; Ok(( StatusCode::OK, @@ -734,14 +785,16 @@ async fn json_metrics() -> Result { } async fn csv_metrics() -> Result { - let metrics_csv = metrics::csv().await; + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + let metrics_csv = fs::read_to_string(&format!("{dir}/metrics.csv")).await?; Ok((StatusCode::OK, [("content-type", "text/csv")], metrics_csv)) } async fn init_metrics() -> Result<()> { - let path = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); - let dir = path::Path::new(&path); + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + let dir = path::Path::new(&dir); + fs::create_dir_all(dir).await?; info!("Starting metrics collection..."); let duration = Instant::now(); @@ -809,6 +862,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)); @@ -819,14 +884,14 @@ async fn main() -> Result<()> { app = app .route("/regtest/block", get(new_block)) .route("/regtest/send/:address/:amount", get(send_coins)); - } else { - tokio::spawn(async { - if let Err(e) = init_metrics().await { - error!("Error in periodic metrics: {e}"); - } - }); } + tokio::spawn(async { + if let Err(e) = init_metrics().await { + error!("Error in init metrics: {e}"); + } + }); + let app = app.layer(CorsLayer::permissive()); let addr = SocketAddr::from(([0, 0, 0, 0], 7070)); diff --git a/src/bitcoin.rs b/src/bitcoin.rs index c734d3ae..f4265bf8 100644 --- a/src/bitcoin.rs +++ b/src/bitcoin.rs @@ -40,7 +40,7 @@ use crate::{ structs::{ DecryptedWalletData, EncryptedWalletDataV04, FundVaultDetails, PublishPsbtRequest, PublishedPsbtResponse, SatsInvoice, SecretString, SignPsbtRequest, SignedPsbtResponse, - WalletData, WalletTransaction, + TransactionData, WalletData, WalletTransaction, }, trace, }; @@ -344,13 +344,13 @@ pub async fn send_sats( destination: &str, // bip21 uri or address amount: u64, fee_rate: Option, -) -> Result { +) -> Result { use payjoin::UriExt; let wallet = get_wallet(descriptor, Some(change_descriptor)).await?; let fee_rate = fee_rate.map(FeeRate::from_sat_per_vb); - let transaction = match payjoin::Uri::try_from(destination) { + let details = match payjoin::Uri::try_from(destination) { Ok(uri) => { let address = uri.address.clone(); validate_address(&address).await?; @@ -373,7 +373,18 @@ pub async fn send_sats( } }; - Ok(transaction) + let vsize = details + .transaction + .as_ref() + .expect("transaction exists") + .vsize(); + let fee_rate = details.fee.expect("fee is present on tx") as f32 / vsize as f32; + + Ok(TransactionData { + details, + vsize, + fee_rate, + }) } pub async fn fund_vault( @@ -540,7 +551,7 @@ pub async fn drain_wallet( descriptor: &SecretString, change_descriptor: Option<&SecretString>, fee_rate: Option, -) -> Result { +) -> Result { let address = Address::from_str(destination)?; validate_address(&address).await?; debug!(format!("Create drain wallet tx to: {address:#?}")); @@ -596,7 +607,18 @@ pub async fn drain_wallet( "Drain wallet transaction submitted with details: {details:#?}" )); - Ok(details) + let vsize = details + .transaction + .as_ref() + .expect("transaction exists") + .vsize(); + let fee_rate = details.fee.expect("fee is present on tx") as f32 / vsize as f32; + + Ok(TransactionData { + details, + vsize, + fee_rate, + }) } else { Err(BitcoinError::DrainWalletNoTxDetails) } diff --git a/src/bitcoin/psbt.rs b/src/bitcoin/psbt.rs index 263bc122..92dfafcf 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, @@ -46,7 +49,7 @@ pub async fn multi_sign_psbt( let mut sign_count = 0; for wallet in wallets { - wallet.lock().await.sign( + let sign = wallet.lock().await.sign( &mut psbt, SignOptions { allow_all_sighashes: true, @@ -55,7 +58,10 @@ pub async fn multi_sign_psbt( }, )?; - sign_count += 1; + if sign { + sign_count += 1; + } + debug!(format!("PSBT Sign: ({sign_count}/{total_wallets})")); } @@ -70,7 +76,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 3ef3a43e..a3da1027 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( @@ -48,11 +53,11 @@ mod server { 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, body).await?; - metrics::update(&filepath).await?; + // metrics::update(&filepath).await?; Ok(()) } - pub async fn server_store( + pub async fn marketplace_store( name: &str, input: &[u8], metadata: Option>, @@ -76,7 +81,44 @@ mod server { 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, body.clone()).await?; - metrics::update(&filepath).await?; + // metrics::update(&filepath).await?; + 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 coordinator_sk = hex::decode(coordinator_key)?; + let coordinator_secret_key = SecretKey::from_slice(&coordinator_sk)?; + let bundle_public_key = + PublicKey::from_str(bundle_id).map_err(|_| CarbonadoError::WrongNostrPublicKey)?; + + let share_sk = SharedSecret::new(&bundle_public_key, &coordinator_secret_key); + let share_sk = share_sk.display_secret().to_string(); + + let sk = hex::decode(share_sk)?; + let secret_key = SecretKey::from_slice(&sk)?; + let public_key = PublicKey::from_secret_key_global(&secret_key); + + let pk = public_key.serialize(); + let pk_hex = public_key.to_hex(); + + 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)) } @@ -122,7 +164,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)?; @@ -146,6 +190,42 @@ 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 coordinator_sk = hex::decode(coordinator_key)?; + let coordinator_secret_key = SecretKey::from_slice(&coordinator_sk)?; + let bundle_public_key = + PublicKey::from_str(bundle_id).map_err(|_| CarbonadoError::WrongNostrPublicKey)?; + + let share_sk = SharedSecret::new(&bundle_public_key, &coordinator_secret_key); + let share_sk = share_sk.display_secret().to_string(); + + let sk = hex::decode(share_sk)?; + let secret_key = SecretKey::from_slice(&sk)?; + let public_key = PublicKey::from_secret_key_global(&secret_key); + + let pk = public_key.to_hex(); + + 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, @@ -212,7 +292,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 { @@ -298,7 +381,7 @@ mod client { } } - pub async fn server_store( + pub async fn marketplace_store( name: &str, input: &[u8], _metadata: Option>, @@ -330,6 +413,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)?; @@ -418,7 +510,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(); @@ -440,6 +534,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 751170c7..f51496cf 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), /// Error: {0} diff --git a/src/carbonado/metrics.rs b/src/carbonado/metrics.rs index aa813db0..3a620200 100644 --- a/src/carbonado/metrics.rs +++ b/src/carbonado/metrics.rs @@ -1,18 +1,18 @@ #![cfg(not(target_arch = "wasm32"))] use std::{ - collections::{BTreeMap, BTreeSet}, + collections::BTreeMap, env, path::{Path, PathBuf}, - sync::Arc, time::SystemTime, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Duration, NaiveDate, Utc}; -use log::debug; +use log::{debug, error}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use tokio::{fs, sync::RwLock}; +use sled::{Config, Db, Mode}; +use tokio::fs; use walkdir::WalkDir; #[derive(Serialize, Deserialize, Default)] @@ -47,12 +47,34 @@ const NETWORK_TOTAL: &str = "total"; const NETWORK_RGB_STOCKS: &str = "rgb_stocks"; const NETWORK_RGB_TRANSFER_FILES: &str = "rgb_transfer_files"; -static METRICS_DATA: Lazy>> = Lazy::new(Default::default); -static METRICS_SET: Lazy>>> = Lazy::new(Default::default); +const DB_PATHS: &str = "PATHS"; +const DB_DAYS: &str = "DAYS"; + +static METRICS_DB: Lazy = Lazy::new(|| { + Config::default() + .path( + PathBuf::from( + env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()), + ) + .join("metrics_sled_kv"), + ) + .mode(Mode::HighThroughput) // Since this uses Tor, disk IO will not be a bottleneck + .compression_factor(19) + .open() + .unwrap_or_else(|e| { + error!( + "Trouble opening Sled keystore: {}. Using a temporary in-memory database.", + e + ); + Config::default() + .temporary(true) + .open() + .expect("temporary sled db") + }) +}); pub async fn init(dir: &Path) -> Result<()> { - let mut metrics = METRICS_DATA.write().await; - let mut dataset = METRICS_SET.write().await; + let mut metrics = MetricsData::default(); metrics .wallets_by_network @@ -85,7 +107,9 @@ pub async fn init(dir: &Path) -> Result<()> { let day_created = metadata.created()?; let day = round_datetime_to_day(day_created.into()); - dataset.insert(entry.path().to_path_buf()); + METRICS_DB + .open_tree(DB_PATHS)? + .insert(entry.path().to_str().unwrap_or("ERROR").as_bytes(), &[1])?; if metadata.is_file() { metrics.bytes += metadata.len(); @@ -264,20 +288,27 @@ pub async fn init(dir: &Path) -> Result<()> { } } + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + + fs::write(&format!("{dir}/metrics.json"), &json(&metrics).await?).await?; + fs::write(&format!("{dir}/metrics.csv"), &csv(&metrics).await).await?; + Ok(()) } pub async fn update(path: &Path) -> Result<()> { debug!("Updating metrics with {path:?}"); - let mut metrics = METRICS_DATA.write().await; - let mut dataset = METRICS_SET.write().await; - - if dataset.get(path).is_some() { + if METRICS_DB + .open_tree(DB_PATHS)? + .contains_key(path.to_str().unwrap_or("ERROR").as_bytes())? + { debug!("Path already present"); return Ok(()); } else { - dataset.insert(path.to_path_buf()); + METRICS_DB + .open_tree(DB_PATHS)? + .insert(path.to_str().unwrap_or("ERROR").as_bytes(), &[1])?; } let filename = path @@ -287,9 +318,102 @@ pub async fn update(path: &Path) -> Result<()> { .to_string(); let metadata = path.metadata()?; let day_created = metadata.created()?; + let day_prior = day_created + .checked_sub(Duration::days(1).to_std()?) + .expect("day exists"); + let day_prior = round_datetime_to_day(day_prior.into()); let day = round_datetime_to_day(day_created.into()); + let first_of_day = if METRICS_DB + .open_tree(DB_DAYS)? + .contains_key(day.as_bytes())? + { + debug!("Day already present"); + false + } else { + METRICS_DB + .open_tree(DB_DAYS)? + .insert(day.as_bytes(), &[1])?; + true + }; + + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + let mut metrics: MetricsData = + serde_json::from_str(&fs::read_to_string(format!("{dir}/metrics.json")).await?)?; + if metadata.is_file() { + if first_of_day { + let bytes_day_prior = { + metrics + .bytes_by_day + .get(&day_prior) + .unwrap_or(&0) + .to_owned() + }; + + metrics + .bytes_by_day + .entry(day.clone()) + .and_modify(|b| *b += bytes_day_prior) + .or_insert(bytes_day_prior); + + let bitcoin_wallets_day_prior = { + metrics + .bitcoin_wallets_by_day + .get(&day_prior) + .unwrap_or(&0) + .to_owned() + }; + + metrics + .bitcoin_wallets_by_day + .entry(day.clone()) + .and_modify(|w| *w += bitcoin_wallets_day_prior) + .or_insert(bitcoin_wallets_day_prior); + + let testnet_wallets_day_prior = { + metrics + .testnet_wallets_by_day + .get(&day_prior) + .unwrap_or(&0) + .to_owned() + }; + + metrics + .testnet_wallets_by_day + .entry(day.clone()) + .and_modify(|w| *w += testnet_wallets_day_prior) + .or_insert(testnet_wallets_day_prior); + + let signet_wallets_day_prior = { + metrics + .signet_wallets_by_day + .get(&day_prior) + .unwrap_or(&0) + .to_owned() + }; + + metrics + .signet_wallets_by_day + .entry(day.clone()) + .and_modify(|w| *w += signet_wallets_day_prior) + .or_insert(signet_wallets_day_prior); + + let regtest_wallets_day_prior = { + metrics + .regtest_wallets_by_day + .get(&day_prior) + .unwrap_or(&0) + .to_owned() + }; + + metrics + .regtest_wallets_by_day + .entry(day.clone()) + .and_modify(|w| *w += regtest_wallets_day_prior) + .or_insert(regtest_wallets_day_prior); + } + metrics.bytes += metadata.len(); *metrics.bytes_by_day.entry(day.clone()).or_insert(0) += metadata.len(); @@ -370,16 +494,14 @@ pub async fn update(path: &Path) -> Result<()> { } } - let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); - // Write metrics to disk as a backup - fs::write(&format!("{dir}/metrics.csv"), &csv().await).await?; - fs::write(&format!("{dir}/metrics.json"), &json().await?).await?; + fs::write(&format!("{dir}/metrics.json"), &json(&metrics).await?).await?; + fs::write(&format!("{dir}/metrics.csv"), &csv(&metrics).await).await?; Ok(()) } -pub async fn csv() -> String { +pub async fn csv(metrics: &MetricsData) -> String { let mut lines = vec![vec![ "Wallet".to_owned(), "Wallet Count".to_owned(), @@ -392,8 +514,6 @@ pub async fn csv() -> String { "Bytes by Day".to_owned(), ]]; - let metrics = METRICS_DATA.read().await; - for (day, bitcoin_wallets) in metrics.bitcoin_wallets_by_day.iter() { let mut line = vec![]; @@ -403,7 +523,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_BITCOIN) - .expect("network is defined") + .unwrap_or(&0) .to_string(), ); line.push(metrics.bytes.to_string()); @@ -415,7 +535,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_TESTNET) - .expect("network is defined") + .unwrap_or(&0) .to_string(), ); line.push("".to_owned()); @@ -427,7 +547,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_SIGNET) - .expect("network is defined") + .unwrap_or(&0) .to_string(), ); line.push("".to_owned()); @@ -439,7 +559,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_REGTEST) - .expect("network is defined") + .unwrap_or(&0) .to_string(), ); line.push("".to_owned()); @@ -451,7 +571,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_TOTAL) - .expect("total is defined") + .unwrap_or(&0) .to_string(), ); line.push("".to_owned()); @@ -463,7 +583,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_RGB_STOCKS) - .expect("rgb_stocks is defined") + .unwrap_or(&0) .to_string(), ); line.push("".to_owned()); @@ -475,7 +595,7 @@ pub async fn csv() -> String { metrics .wallets_by_network .get(NETWORK_RGB_TRANSFER_FILES) - .expect("rgb_transfer_files is defined") + .unwrap_or(&0) .to_string(), ); line.push("".to_owned()); @@ -519,10 +639,8 @@ pub async fn csv() -> String { lines.join("\n") } -pub async fn json() -> Result { - let metrics = METRICS_DATA.read().await; - - Ok(serde_json::to_string_pretty(&*metrics)?) +pub async fn json(metrics: &MetricsData) -> Result { + Ok(serde_json::to_string_pretty(metrics)?) } fn round_datetime_to_day(datetime: DateTime) -> String { diff --git a/src/constants.rs b/src/constants.rs index e838f4ec..c9e593be 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -60,6 +60,9 @@ pub static MARKETPLACE_FEE_PERC: Lazy> = pub static MARKETPLACE_FEE_XPUB: Lazy> = Lazy::new(|| RwLock::new(dot_env("MARKETPLACE_FEE_XPUB"))); +pub static COORDINATOR_NOSTR: Lazy> = + Lazy::new(|| RwLock::new(dot_env("COORDINATOR_NOSTR"))); + pub async fn get_marketplace_seed() -> String { MARKETPLACE_SEED.read().await.to_string() } @@ -76,6 +79,10 @@ pub async fn get_marketplace_fee_xpub() -> String { MARKETPLACE_FEE_XPUB.read().await.to_string() } +pub async fn get_coordinator_nostr_key() -> String { + COORDINATOR_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 +227,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/regtest.rs b/src/regtest.rs index 4edbdcd8..fd59b01d 100644 --- a/src/regtest.rs +++ b/src/regtest.rs @@ -1,6 +1,25 @@ #![cfg(not(target_arch = "wasm32"))] -use std::env; -use std::process::{Command, Stdio}; +use std::{ + env, fs, path, + process::{Command, Stdio}, +}; + +use anyhow::Result; + +use crate::carbonado::metrics::MetricsData; + +pub fn init_fs() -> Result<()> { + let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); + let dir = path::Path::new(&dir); + + fs::create_dir_all(dir)?; + fs::write( + dir.join("metrics.json"), + serde_json::to_string_pretty(&MetricsData::default())?, + )?; + + Ok(()) +} pub fn send_coins(address: &str, amount: &str) { let path = env::current_dir().expect("oh no!"); diff --git a/src/rgb.rs b/src/rgb.rs index b4b60ed2..7bb3abb9 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -2,14 +2,14 @@ use ::psbt::{serialize::Serialize, Psbt}; use amplify::{confinement::U32, hex::ToHex}; use anyhow::Result; use autosurgeon::reconcile; -use bitcoin::{psbt::PartiallySignedTransaction, Network, Txid}; +use bitcoin::{psbt::PartiallySignedTransaction as PsbtV0, Network, Txid}; use bitcoin_30::bip32::ExtendedPubKey; use bitcoin_scripts::address::AddressNetwork; use futures::TryFutureExt; use garde::Validate; use miniscript_crate::DescriptorPublicKey; -use rgb::{RgbDescr, RgbWallet}; +use rgb::RgbDescr; use rgbstd::{ containers::BindleContent, contract::ContractId, @@ -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,18 @@ use crate::{ IssueMediaRequest, IssueRequest, IssueResponse, MediaEncode, MediaRequest, MediaResponse, MediaView, NextAddressResponse, NextUtxoResponse, NextUtxosResponse, PsbtFeeRequest, PsbtRequest, PsbtResponse, PublicRgbBidResponse, PublicRgbOfferResponse, - PublicRgbOffersResponse, ReIssueRequest, ReIssueResponse, RgbBidDetail, RgbBidRequest, + PublicRgbOffersResponse, PublishPsbtRequest, ReIssueRequest, ReIssueResponse, + RgbAuctionBidRequest, RgbAuctionBidResponse, RgbAuctionFinishResponse, + RgbAuctionOfferRequest, RgbAuctionOfferResponse, RgbBidDetail, RgbBidRequest, RgbBidResponse, RgbBidsResponse, RgbInternalSaveTransferRequest, RgbInternalTransferResponse, RgbInvoiceResponse, RgbOfferBidsResponse, RgbOfferDetail, RgbOfferRequest, RgbOfferResponse, RgbOfferUpdateRequest, RgbOfferUpdateResponse, RgbOffersResponse, RgbRemoveTransferRequest, RgbReplaceResponse, RgbSaveTransferRequest, - RgbSwapRequest, RgbSwapResponse, RgbTransferDetail, RgbTransferRequest, + RgbSwapItem, RgbSwapRequest, RgbSwapResponse, 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, }; @@ -106,17 +111,19 @@ use self::{ post_consignments, post_media_metadata, post_media_metadata_list, ProxyError, }, psbt::{ - save_tap_commit_str, set_tapret_output, CreatePsbtError, EstimateFeeError, NewPsbtOptions, + save_rgb_commit_str, set_tapret_output, CreatePsbtError, EstimateFeeError, NewPsbtOptions, }, structs::{ ContractAmount, ContractBoilerplate, MediaMetadata, RgbAccountV1, RgbExtractTransfer, 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, + 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, }, transfer::{extract_transfer, AcceptTransferError, NewInvoiceError, NewPaymentError}, wallet::{ @@ -677,10 +684,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()); @@ -748,7 +755,7 @@ pub async fn full_transfer_asset( _ => return Err(TransferError::NoWatcher), }; - save_tap_commit_str( + save_rgb_commit_str( &outpoint, amount, &commit, @@ -814,7 +821,7 @@ pub async fn transfer_asset( _ => return Err(TransferError::NoWatcher), }; - save_tap_commit_str( + save_rgb_commit_str( &outpoint, amount, &commit, @@ -853,6 +860,10 @@ pub enum RgbSwapError { NoContract, /// Available Utxo is required in this operation. {0} NoUtxo(String), + /// Offer {0} not found. + NoOffer(String), + /// Bundle {0} not found. + NoBundle(String), /// The Offer has expired. OfferExpired, /// Insufficient funds (expected: {input} sats / current: {output} sats) @@ -871,7 +882,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), @@ -879,6 +892,8 @@ pub enum RgbSwapError { Swap(RgbOfferErrors), /// Occurs an error in transfer step. {0} Transfer(TransferError), + /// Offer {0} is valid. Reason {1} + WrongOffer(String, String), /// Swap fee cannot be decoded. {0} WrongSwapFee(String), /// Request order contains wrong contract precision. expect: {0} / current: {1}. @@ -889,6 +904,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 +914,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 +930,82 @@ 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_psbt, + bundle_id: None, + }; + + 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: RgbAuctionOfferRequest, +) -> 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 +1015,104 @@ 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.offers.clone() { + let mut new_offer = internal_create_seller_offer( + sk, + item, + options.clone(), + &mut rgb_account, + &mut stock, + &mut resolver, + ) + .await?; + + let RgbOffer { + offer_id, + contract_id, + asset_amount, + asset_precision, + seller_address, + bitcoin_price, + seller_psbt, + strategy, + bundle_id, + .. + } = 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"); + + let request = SignPsbtRequest { + psbt: seller_psbt, + descriptors: request.sign_keys.clone(), + }; + + let SignedPsbtResponse { + psbt: final_psbt, .. + } = sign_psbt_file(request) + .await + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + new_offer.seller_psbt = final_psbt.clone(); + + my_offers = my_offers.save_offer(contract_id.clone(), new_offer.clone()); + collection.push(RgbOfferSwap::from(new_offer.clone())); + + resp.push(RgbOfferResponse { + offer_id, + contract_id: contract_id.clone(), + contract_amount, + bitcoin_price, + seller_address: seller_address.to_string(), + seller_psbt: final_psbt, + bundle_id, + }); + } + + 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 +1124,7 @@ pub async fn create_seller_offer( bitcoin_price, iface, expire_at, - presig, + strategy, change_terminal, .. } = request.clone(); @@ -945,10 +1135,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 +1156,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 +1232,225 @@ 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, + pub_key: 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 (mut new_bid, resp) = internal_create_buyer_bid( + sk, + buyer_bid_req, + &mut rgb_account, + &mut stock, + &mut resolver, + ) + .await?; + + let RgbBid { + iface, + contract_id, + buyer_psbt, + .. + } = new_bid.clone(); + + let RgbBidResponse { + bid_id, + offer_id, + invoice: buyer_invoice, + swap_psbt, + fee_value, + } = resp.clone(); + + let RgbOfferSwap { + strategy, + pub_key: offer_pub, + expire_at, + .. + } = get_public_offer(offer_id.clone()) + .await + .map_err(RgbSwapError::Buyer)?; + + let mut my_bids = retrieve_bids(sk).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 sign_req = SignPsbtRequest { + psbt: final_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 sign_req = SignPsbtRequest { + psbt: buyer_psbt.unwrap_or_default(), + descriptors: request.sign_keys, + }; + + let SignedPsbtResponse { + psbt: buyer_swap_psbt, + .. + } = sign_psbt_file(sign_req) + .await + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + + new_bid.transfer_id = Some(consig_id.clone()); + new_bid.transfer = Some(final_consig.clone()); + my_bids = my_bids.save_bid(contract_id, new_bid.clone()); + + let mut bid_swap = RgbBidSwap::from(new_bid); + publish_public_bid(bid_swap.clone()) + .await + .map_err(RgbSwapError::Marketplace)?; + + bid_swap.swap_outpoint = Some(outpoint); + bid_swap.swap_amount = Some(amount); + bid_swap.swap_commit = Some(commit); + bid_swap.buyer_psbt = None; + bid_swap.swap_psbt = None; + + publish_swap_bid(sk, &offer_pub, bid_swap.clone(), expire_at) + .await + .map_err(RgbSwapError::Auction)?; + + bid_swap.buyer_psbt = Some(buyer_swap_psbt.clone()); + bid_swap.swap_psbt = Some(final_swap_psbt.clone()); + publish_auction_bid(bid_swap) + .await + .map_err(RgbSwapError::Auction)?; + + store_bids(sk, my_bids).await.map_err(RgbSwapError::IO)?; + } + 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, @@ -1106,18 +1459,52 @@ pub async fn create_buyer_bid( let RgbOfferSwap { iface, - seller_psbt, - public: offer_pub, + bitcoin_price, expire_at, + bundle_id, + seller_psbt, .. - } = get_public_offer(offer_id) + } = get_public_offer(offer_id.clone()) .await .map_err(RgbSwapError::Buyer)?; + let seller_psbt = match seller_psbt { + Some(psbt) => psbt, + None => { + let bundle_id = bundle_id.unwrap_or_default(); + match get_auction_offer(&bundle_id, offer_id.clone()) + .await + .map_err(RgbSwapError::Buyer)? + { + Some(auction_offer) => auction_offer.seller_psbt.unwrap_or_default(), + _ => return Err(RgbSwapError::NoOffer(offer_id.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), + }; + 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,32 +1534,23 @@ pub async fn create_buyer_bid( ..default!() }; - let buyer_psbt = internal_create_psbt(psbt_req, &mut rgb_account, &mut resolver, Some(options)) + let PsbtResponse { + psbt: 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(); + new_bid.buyer_psbt = Some(buyer_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()))?; - let buyer_psbt = Psbt::from_str(&buyer_psbt.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 = PartiallySignedTransaction::from(seller_psbt); - let buyer_psbt = PartiallySignedTransaction::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) @@ -1189,14 +1567,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 +1575,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 +1590,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 +1619,28 @@ pub async fn create_swap_transfer( let RgbOfferSwap { iface, expire_at, - presig, - public: offer_pub, + pub_key: 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, + pub_key: buyer_pub, .. } = rgb_swap_bid.clone(); let change_terminal = match iface.to_uppercase().as_str() { @@ -1323,47 +1679,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)?; + update_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.swap_outpoint = Some(outpoint); + rgb_swap_bid.swap_amount = Some(amount); + rgb_swap_bid.swap_commit = Some(commit); + offer_pub + } + RgbSwapStrategy::HotSwap => { + let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + update_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_rgb_commit_str( + &outpoint, + amount, + &commit, + &my_offer.terminal, + &mut rgb_wallet, + ); + rgb_account + .wallets + .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet); + } } + buyer_pub } - } + 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()); @@ -1447,14 +1808,22 @@ async fn internal_transfer_asset( let iface = rgb_invoice.clone().iface.unwrap().to_string(); let mut consigs = BTreeMap::default(); - for (pos, invoice) in options.other_invoices.into_iter().enumerate() { - let current_transfer = &transfers[pos]; - let current_transfer = current_transfer - .to_strict_serialized::<{ U32 }>() - .map_err(|err| TransferError::WrongConsig(err.to_string()))?; - - let current_transfer = current_transfer.to_hex(); - consigs.insert(invoice.beneficiary.to_string(), current_transfer); + + // 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( @@ -1518,7 +1887,7 @@ pub async fn internal_replace_transfer( _ => return Err(TransferError::NoWatcher), }; - save_tap_commit_str( + save_rgb_commit_str( &outpoint, amount, &commit, @@ -1545,6 +1914,236 @@ pub async fn internal_replace_transfer( Ok(resp) } +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()) + .await + .map_err(RgbSwapError::Auction)?; + + let mut final_psbt: Option = none!(); + for bid in bids.clone() { + if let Some(offer) = offers + .clone() + .into_iter() + .find(|x| x.offer_id == bid.offer_id) + { + 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 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()))?; + + final_psbt = if let Some(final_psbt) = final_psbt { + let final_psbt = final_psbt + .join(swap_part.clone()) + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + + Some(final_psbt) + } else { + Some(swap_part) + } + } + } + + let mut resp = RgbAuctionFinishResponse { + bundle_id: bundle_id.clone(), + ..Default::default() + }; + if let Some(RgbBidSwap { + iface, + buyer_invoice, + .. + }) = bids.clone().get(0) + { + let all_invoices = bids + .clone() + .into_iter() + .skip(1) + .map(|x| RgbInvoice::from_str(&x.buyer_invoice).expect("")) + .collect(); + + let change_terminal = match iface.to_uppercase().as_str() { + "RGB20" => "/20/1", + "RGB21" => "/21/1", + _ => "/10/1", + }; + + let options = NewTransferOptions::with(true, all_invoices); + let final_psbt = Psbt::from(final_psbt.unwrap()); + let final_psbt = Serialize::serialize(&final_psbt).to_hex(); + let request = RgbTransferRequest { + psbt: final_psbt, + rgb_invoice: buyer_invoice.clone(), + terminal: change_terminal.to_string(), + }; + + let RgbInternalTransferResponse { + consig_id, + psbt: final_psbt, + consig: final_consig, + consigs, + commit, + outpoint, + .. + } = internal_transfer_asset( + request, + options.clone(), + &mut stock, + &mut rgb_account, + &mut rgb_transfers, + ) + .await + .map_err(RgbSwapError::Transfer)?; + + publish_psbt_file(PublishPsbtRequest { + psbt: final_psbt.clone(), + }) + .await + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + + resp.outpoint = outpoint.clone(); + for mut bid in bids.clone() { + let RgbBidSwap { + pub_key: counter_party_key, + buyer_invoice, + .. + } = bid.clone(); + if let Some(RgbOffer { expire_at, .. }) = offers + .clone() + .into_iter() + .find(|x| x.offer_id == bid.offer_id) + { + let RgbInvoice { beneficiary, .. } = + RgbInvoice::from_str(&buyer_invoice).expect("invalid invoice"); + + if let Some(strict_consig) = consigs.get(&beneficiary.to_string()) { + let (_, consig) = extract_transfer(strict_consig.clone()).expect(""); + bid.transfer_id = Some(consig.bindle_id().to_string()); + bid.transfer = Some(strict_consig.clone()); + } else { + bid.transfer_id = Some(consig_id.clone()); + bid.transfer = Some(final_consig.clone()); + } + + bid.swap_outpoint = Some(outpoint.clone()); + bid.swap_commit = Some(commit.clone()); + + publish_swap_bid(sk, &counter_party_key, bid, expire_at) + .await + .map_err(RgbSwapError::Swap)?; + } + } + + let final_psbt = Psbt::from_str(&final_psbt) + .map_err(|op| RgbSwapError::WrongPsbtFinal(op.to_string()))?; + + let internal_request = RgbInternalSaveTransferRequest::with( + consig_id.clone(), + final_consig.clone(), + buyer_invoice.to_string(), + iface.to_string(), + true, + Some(consigs.clone()), + Some(final_psbt), + ); + + internal_save_transfer(internal_request, &mut rgb_transfers) + .await + .map_err(|op| RgbSwapError::WrongConsigSwap(op.to_string()))?; + } + + store_stock_account_transfers(sk, stock, rgb_account, rgb_transfers) + .await + .map_err(RgbSwapError::IO)?; + + // Retrieve Auctions Results + let mut sold_items = bmap! {}; + let mut remaining_items = bmap! {}; + for RgbOffer { + contract_id: offer_contract, + iface: offer_iface, + offer_id, + asset_amount: offer_amount, + asset_precision: offer_precision, + .. + } in offers.into_iter().clone() + { + if let Some(bid) = bids.clone().into_iter().find(|x| x.offer_id == offer_id) { + let remaining = offer_amount - bid.asset_amount; + let contract_amount = + ContractAmount::new(bid.asset_amount, bid.asset_precision).to_string(); + sold_items.insert( + offer_id.clone(), + RgbSwapItem { + contract_id: offer_contract.clone(), + iface: offer_iface.clone(), + contract_amount, + }, + ); + + if remaining > 0 { + let contract_amount = + ContractAmount::new(offer_amount, offer_precision).to_string(); + remaining_items.insert( + offer_id.clone(), + RgbSwapItem { + contract_id: offer_contract, + iface: offer_iface, + contract_amount, + }, + ); + } + } else { + let contract_amount = ContractAmount::new(offer_amount, offer_precision).to_string(); + remaining_items.insert( + offer_id, + RgbSwapItem { + contract_id: offer_contract, + iface: offer_iface, + contract_amount, + }, + ); + } + } + + resp.remaining = remaining_items; + resp.sold = sold_items; + + Ok(resp) +} + +pub async fn list_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.bundle_id.is_some() + && x.expire_at.unwrap_or_default().sub(utc) <= 0 + }) + .map(RgbAuctionOfferResponse::from) + .collect(); + + Ok(auction_offers) +} + pub async fn accept_transfer( sk: &str, request: AcceptRequest, @@ -1772,9 +2371,8 @@ pub async fn verify_transfers(sk: &str) -> Result Result Result Result Result Result<(), SaveTransferError> { let mut my_swaps = vec![]; - let my_offers = retrieve_offers(sk).await.map_err(SaveTransferError::IO)?; let mut current_offers = vec![]; + let my_offers = retrieve_offers(sk).await.map_err(SaveTransferError::IO)?; my_offers .offers .values() - .for_each(|bs| current_offers.extend(bs)); + .for_each(|bs| current_offers.extend(bs.to_owned())); - current_offers.retain(|x| x.offer_status != RgbOrderStatus::Fill); + let mut rgb_wallet = rgb_accounts.wallets.get(RGB_DEFAULT_NAME).unwrap().clone(); + current_offers.retain(|x| !x.clone().is_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 { let RgbBidSwap { - tap_commit, - tap_outpoint, - tap_amount, + swap_commit, + swap_outpoint, + swap_amount, .. } = swap_bid; - if tap_commit.is_some() && tap_outpoint.is_some() { - save_tap_commit_str( - &tap_outpoint.unwrap_or_default(), - tap_amount.unwrap_or_default(), - &tap_commit.unwrap_or_default(), + if swap_commit.is_some() && swap_outpoint.is_some() { + save_rgb_commit_str( + &swap_outpoint.unwrap_or_default(), + swap_amount.unwrap_or_default(), + &swap_commit.unwrap_or_default(), &offer.terminal, - rgb_wallet, + &mut rgb_wallet, ); } } } } + rgb_accounts + .wallets + .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet); let my_bids = retrieve_bids(sk).await.map_err(SaveTransferError::IO)?; let mut current_bids = vec![]; - my_bids.bids.values().for_each(|bs| current_bids.extend(bs)); + my_bids + .bids + .values() + .for_each(|bs| current_bids.extend(bs.to_owned())); - current_bids.retain(|x| x.bid_status != RgbOrderStatus::Fill); + current_bids.retain(|x| !x.clone().is_fill()); for bid in current_bids { if let Ok(swaps_bid) = get_swap_bid_by_buyer(sk, bid.offer_id.clone(), bid.bid_id.clone()).await @@ -1927,7 +2528,7 @@ pub async fn internal_swap_transfers( Ok(()) } -pub async fn internal_update_transfers( +pub async fn internal_extract_transfers_seals( rgb_account: RgbAccountV1, rgb_transfers: &mut RgbTransfersV1, ) -> Result, TransferError> { 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..802f9a02 100644 --- a/src/rgb/cambria.rs +++ b/src/rgb/cambria.rs @@ -1,8 +1,8 @@ -use crate::rgb::structs::{RgbAccountV0, RgbAccountV1}; +use crate::rgb::structs::{ + RgbAccountV0, RgbAccountV1, RgbTransferV0, RgbTransferV1, RgbTransfersV0, RgbTransfersV1, +}; use postcard::from_bytes; -use super::structs::{RgbTransferV0, RgbTransferV1, RgbTransfersV0, RgbTransfersV1}; - #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum ModelVersionError { @@ -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..aa11cbe5 100644 --- a/src/rgb/carbonado.rs +++ b/src/rgb/carbonado.rs @@ -5,26 +5,23 @@ 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::rgb::crdt::{LocalRgbAccount, LocalRgbOffers, RawRgbAccount}; - -use crate::rgb::swap::{RgbBids, RgbOffers}; use crate::{ - carbonado::{retrieve, server_retrieve, store}, + carbonado::{ + auctions_retrieve, auctions_store, marketplace_retrieve, marketplace_store, retrieve, store, + }, rgb::{ - cambria::{ModelVersion, RgbAccountVersions}, + cambria::{ModelVersion, RgbAccountVersions, RgbtransferVersions}, constants::RGB_STRICT_TYPE_VERSION, - crdt::LocalRgbOfferBid, - structs::RgbAccountV1, + crdt::{ + LocalRgbAccount, LocalRgbAuctions, LocalRgbOfferBid, LocalRgbOffers, RawRgbAccount, + }, + structs::{RgbAccountV1, RgbTransfersV1}, + swap::{RgbAuctionSwaps, RgbBidSwap, RgbBids, RgbOffers, RgbPublicSwaps}, }, }; -use super::cambria::RgbtransferVersions; -use super::structs::RgbTransfersV1; -use super::swap::{PublicRgbOffers, RgbBidSwap}; - -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 +328,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 +459,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 +473,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, _) = auctions_retrieve(bundle_id, 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 +548,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 +572,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 +595,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/consignment.rs b/src/rgb/consignment.rs index d5649779..13b0db6b 100644 --- a/src/rgb/consignment.rs +++ b/src/rgb/consignment.rs @@ -10,7 +10,7 @@ use bp::{Outpoint, Txid}; use chrono::Utc; use rgbstd::{ containers::{Bindle, BuilderSeal, Transfer}, - contract::{ContractId, GraphSeal, Operation, Opout, SecretSeal}, + contract::{ContractId, GraphSeal, Operation, Opout, SecretSeal, Transition}, interface::{BuilderError, ContractSuppl, TypedState, VelocityHint}, persistence::{Inventory, Stash}, schema::AssignmentType, @@ -20,6 +20,35 @@ use rgbwallet::{ Beneficiary, PayError, RgbInvoice, }; use seals::txout::CloseMethod; +use strict_encoding::TypeName; + +#[derive(Clone, Debug, Display, Error, From)] +#[display(doc_comments)] +pub struct TransitionContainer { + pub state: StateContainer, + pub beneficiary: BuilderSeal, + pub transition: Transition, +} + +#[derive(Clone, Debug, Display, Error, From)] +#[display(doc_comments)] +pub struct StateContainer { + pub iface: TypeName, + pub contract_id: ContractId, + pub opouts: BTreeMap, + pub states: Vec, +} + +impl StateContainer { + pub fn new(contract_id: ContractId, iface: TypeName) -> Self { + Self { + contract_id, + iface, + opouts: bmap! {}, + states: vec![], + } + } +} #[derive(Clone, Debug, Display, Error, From)] #[display(doc_comments)] @@ -59,6 +88,410 @@ pub trait ConsignmentEx: Inventory { /// 1. If PSBT output has BIP32 derivation information it belongs to our /// wallet - except when it matches address from the invoice. #[allow(clippy::result_large_err, clippy::type_complexity)] + fn pay_all( + &mut self, + invoice: RgbInvoice, + psbt: &mut Psbt, + method: CloseMethod, + options: NewTransferOptions, + ) -> Result>, PayError::Error>> + where + Self::Error: From<::Error>, + { + // 1. Validations + let RgbInvoice { + expiry: current_expiry, + iface: current_iface, + contract: current_contract_id, + .. + } = invoice.clone(); + + if let Some(expiry) = current_expiry { + if expiry < Utc::now().timestamp() { + return Err(PayError::InvoiceExpired); + } + } + + let current_contract_id = current_contract_id.ok_or(PayError::NoContract)?; + let current_iface = current_iface.ok_or(PayError::NoIface)?; + + // 1. Prepare the data (All States) + let NewTransferOptions { other_invoices, .. } = options.clone(); + let current_state = StateContainer::new(current_contract_id, current_iface.clone()); + + let mut other_states: Vec<_> = other_invoices + .clone() + .into_iter() + .filter(|x| x.contract.unwrap() != current_contract_id) + .map(|x| (x.contract.unwrap(), x.iface.unwrap())) + .collect(); + other_states.sort(); + other_states.dedup(); + + let mut other_states: Vec<_> = other_states + .into_iter() + .map(|(contract_id, iface)| StateContainer::new(contract_id, iface)) + .collect(); + other_states.insert(0, current_state); + + let prev_outputs = psbt + .unsigned_tx + .input + .iter() + .map(|txin| txin.previous_output) + .map(|outpoint| Outpoint::new(outpoint.txid.to_byte_array().into(), outpoint.vout)) + .collect::>(); + + // Classify PSBT outputs which can be used for assignments + let mut out_classes = HashMap::>::new(); + for (no, outp) in psbt.outputs.iter().enumerate() { + if outp + // NB: Here we assume that if output has derivation information it belongs to our wallet. + .bip32_derivation + .first_key_value() + .map(|(_, src)| src) + .or_else(|| { + outp.tap_key_origins + .first_key_value() + .map(|(_, (_, src))| src) + }) + .and_then(|(_, src)| src.into_iter().rev().nth(1)) + .copied() + .map(u32::from) + .is_some() + { + let class = outp.rgb_velocity_hint().unwrap_or_default(); + out_classes.entry(class).or_default().push(no as u32); + } + } + let mut out_classes = out_classes + .into_iter() + .map(|(class, indexes)| (class, indexes.into_iter().cycle())) + .collect::>(); + + let mut output_for_assignment = |suppl: Option<&ContractSuppl>, + assignment_type: AssignmentType| + -> Option> { + let velocity = suppl + .and_then(|suppl| suppl.owned_state.get(&assignment_type)) + .map(|s| s.velocity) + .unwrap_or_default(); + if let Some(vout) = out_classes + .get_mut(&velocity) + .and_then(iter::Cycle::next) + .or_else(|| { + out_classes + .get_mut(&VelocityHint::default()) + .and_then(iter::Cycle::next) + }) + { + let seal = GraphSeal::new_vout(method, vout); + Some(BuilderSeal::Revealed(seal)) + } else { + None + } + }; + + let mut default_change_seal: Option> = none!(); + let mut states_transitions = vec![]; + for mut state_container in other_states { + let contract_id = state_container.contract_id; + + let mut invoices: Vec<_> = other_invoices + .clone() + .into_iter() + .filter(|x| x.contract.unwrap() == state_container.contract_id) + .collect(); + + let current_invoice = if current_contract_id == contract_id { + invoice.clone() + } else { + invoices.remove(0) + }; + + let iface = current_invoice.iface.ok_or(PayError::NoIface)?; + let (_, beneficiary) = match current_invoice.beneficiary { + Beneficiary::BlindedSeal(seal) => { + let seal = BuilderSeal::Concealed(seal); + (None, seal) + } + Beneficiary::WitnessUtxo(addr) => { + let vout = psbt + .unsigned_tx + .output + .iter() + .enumerate() + .find(|(_, txout)| txout.script_pubkey == addr.script_pubkey()) + .map(|(no, _)| no as u32) + .ok_or(PayError::NoBeneficiaryOutput)?; + let seal = BuilderSeal::Revealed(GraphSeal::new_vout(method, vout)); + (Some(vout), seal) + } + }; + + let mut builder = + self.transition_builder(contract_id, iface.clone(), None::)?; + + // 2. Prepare and self-consume transition + let assignment_name = current_invoice + .assignment + .as_ref() + .or_else(|| builder.default_assignment().ok()) + .ok_or(BuilderError::NoDefaultAssignment)?; + + let assignment_id = builder + .assignments_type(assignment_name) + .ok_or(BuilderError::InvalidStateField(assignment_name.clone()))?; + + // TODO: select supplement basing on the signer trust level + let suppl = self + .contract_suppl(contract_id) + .and_then(|set| set.first()) + .cloned(); + + state_container.opouts = + self.state_for_outpoints(contract_id, prev_outputs.iter().copied())?; + + let mut sum_inputs = 0u64; + let mut type_state = TypedState::Void; + for (opout, state) in state_container.opouts.clone() { + builder = builder.add_input(opout)?; + if opout.ty != assignment_id { + if default_change_seal.is_none() { + default_change_seal = output_for_assignment(suppl.as_ref(), opout.ty); + } + + if let Some(seal) = default_change_seal { + builder = builder.add_raw_state(opout.ty, seal, state)?; + } + } else if let TypedState::Amount(value) = state { + sum_inputs += value; + type_state = state; + } else if let TypedState::Data(_) = state { + sum_inputs += 1; + type_state = state; + } + } + + // Retrieve Previous State Transtitions + let mut previous_state_value = 0u64; + for invoice in invoices { + match invoice.owned_state { + TypedState::Amount(value) => { + previous_state_value += value; + } + TypedState::Data(_) => { + previous_state_value += 1; + } + _ => { + todo!("only TypedState::Amount is currently supported") + } + } + + let seal = match invoice.beneficiary { + Beneficiary::BlindedSeal(seal) => seal, + Beneficiary::WitnessUtxo(addr) => { + let vout = psbt + .unsigned_tx + .output + .iter() + .enumerate() + .find(|(_, txout)| txout.script_pubkey == addr.script_pubkey()) + .map(|(no, _)| no as u32) + .ok_or(PayError::NoBeneficiaryOutput)?; + GraphSeal::new_vout(method, vout).to_concealed_seal() + } + }; + + let prev_seal = PreviousSeal { + state: invoice.owned_state, + seal, + }; + state_container.states.push(prev_seal); + } + + for PreviousSeal { seal, state } in state_container.states.clone() { + let prev_seal = BuilderSeal::Concealed(seal); + builder = match state { + TypedState::Amount(value) => builder.add_raw_state( + assignment_id, + prev_seal, + TypedState::Amount(value), + )?, + TypedState::Data(_) => { + builder.add_raw_state(assignment_id, prev_seal, state)? + } + _ => { + todo!( + "Only TypedState::Amount and TypedState::Data are currently supported" + ) + } + }; + } + + // Add change + let amt = match invoice.owned_state { + TypedState::Amount(amt) => { + if sum_inputs < amt + previous_state_value { + return Err(PayError::InsufficientState); + } + + match sum_inputs.cmp(&amt) { + Ordering::Greater => { + if default_change_seal.is_none() { + default_change_seal = + output_for_assignment(suppl.as_ref(), assignment_id); + } + + if let Some(seal) = default_change_seal { + let change = amt + previous_state_value; + let change = TypedState::Amount(sum_inputs - change); + builder = builder.add_raw_state(assignment_id, seal, change)?; + } + amt + } + Ordering::Equal => amt, + Ordering::Less => return Err(PayError::InsufficientState), + } + } + _ => { + todo!("only TypedState::Amount is currently supported") + } + }; + + // Finish Transition + let transition = match type_state { + TypedState::Amount(_) => builder + .add_raw_state(assignment_id, beneficiary, TypedState::Amount(amt))? + .complete_transition(contract_id)?, + TypedState::Data(_) => builder + .add_raw_state(assignment_id, beneficiary, type_state)? + .complete_transition(contract_id)?, + _ => { + todo!("Only TypedState::Amount and TypedState::Data are currently supported") + } + }; + + states_transitions.push(TransitionContainer { + state: state_container.clone(), + beneficiary, + transition, + }); + } + + // 3. Prepare and self-consume other transitions + let all_contracts: Vec<_> = states_transitions + .clone() + .into_iter() + .map(|x| x.state.contract_id) + .collect(); + + let mut contract_inputs = HashMap::>::new(); + let mut spent_state = HashMap::>::new(); + for outpoint in prev_outputs { + for id in self.contracts_by_outpoints([outpoint])? { + contract_inputs.entry(id).or_default().push(outpoint); + if all_contracts.contains(&id) { + continue; + } + spent_state + .entry(id) + .or_default() + .extend(self.state_for_outpoints(id, [outpoint])?); + } + } + + // Construct blank transitions, self-consume them + let mut other_transitions = HashMap::with_capacity(spent_state.len()); + for (id, opouts) in spent_state { + let mut blank_builder = self.blank_builder(id, current_iface.clone())?; + // TODO: select supplement basing on the signer trust level + let suppl = self.contract_suppl(id).and_then(|set| set.first()); + + for (opout, state) in opouts { + if default_change_seal.is_none() { + default_change_seal = output_for_assignment(suppl, opout.ty); + } + + if let Some(seal) = default_change_seal { + blank_builder = blank_builder + .add_input(opout)? + .add_raw_state(opout.ty, seal, state)?; + } + } + + other_transitions.insert(id, blank_builder.complete_transition(id)?); + } + + // 4. Add transitions to PSBT + states_transitions.clone().into_iter().for_each(|x| { + other_transitions.insert(x.state.contract_id, x.transition); + }); + + for (id, transition) in other_transitions.clone() { + let inputs: &Vec = contract_inputs.get(&id).unwrap(); + for (input, txin) in psbt.inputs.iter_mut().zip(&psbt.unsigned_tx.input) { + let prevout = txin.previous_output; + let outpoint = Outpoint::new(prevout.txid.to_byte_array().into(), prevout.vout); + if inputs.contains(&outpoint) { + input.set_rgb_consumer(id, transition.id())?; + } + } + psbt.push_rgb_transition(transition)?; + } + + // Here we assume the provided PSBT is final: its inputs and outputs will not be + // modified after calling this method. + let bundles = psbt.rgb_bundles()?; + // TODO: Make it two-staged, such that PSBT editing will be allowed by other + // participants as required for multiparty protocols like coinjoin. + psbt.rgb_bundle_to_lnpbp4()?; + let anchor = psbt.dbc_conclude(method)?; + // TODO: Ensure that with PSBTv2 we remove flag allowing PSBT modification. + + // 5. Prepare transfer + let witness_txid = psbt.unsigned_tx.txid(); + self.consume_anchor(anchor)?; + for (id, bundle) in bundles { + self.consume_bundle(id, bundle, witness_txid.to_byte_array().into())?; + } + + // 6.Prepare strict transfers + let mut transfers = vec![]; + for state_container in states_transitions.iter().cloned() { + let mut beneficiaries = vec![]; + let current_beneficiary = match state_container.beneficiary { + BuilderSeal::Revealed(seal) => BuilderSeal::Revealed( + seal.resolve(Txid::from_byte_array(witness_txid.to_byte_array())), + ), + BuilderSeal::Concealed(seal) => BuilderSeal::Concealed(seal), + }; + + beneficiaries.push(current_beneficiary); + state_container.state.states.into_iter().for_each(|x| { + beneficiaries.push(BuilderSeal::Concealed(x.seal)); + }); + + if options.strict { + for item in beneficiaries { + let transfer = self.transfer(state_container.state.contract_id, vec![item])?; + transfers.push(transfer); + } + } else { + let transfer = self.transfer(state_container.state.contract_id, beneficiaries)?; + transfers.push(transfer); + } + } + + Ok(transfers) + } + + /// # Assumptions + /// + /// 1. If PSBT output has BIP32 derivation information it belongs to our + /// wallet - except when it matches address from the invoice. + #[deprecated] + #[allow(clippy::result_large_err, clippy::type_complexity)] fn process( &mut self, invoice: RgbInvoice, diff --git a/src/rgb/constants.rs b/src/rgb/constants.rs index f7b2123a..0753a322 100644 --- a/src/rgb/constants.rs +++ b/src/rgb/constants.rs @@ -7,6 +7,8 @@ pub const RGB_OLDEST_VERSION: [u8; 8] = [0; 8]; pub const RGB_STRICT_TYPE_VERSION: [u8; 8] = *b"rgbst161"; pub const RGB_DEFAULT_FETCH_LIMIT: u32 = 10; pub const BITCOIN_DEFAULT_FETCH_LIMIT: u32 = 20; +pub const RGB20_DERIVATION_INDEX: u32 = 20; +pub const RGB21_DERIVATION_INDEX: u32 = 21; // General Errors #[cfg(target_arch = "wasm32")] diff --git a/src/rgb/crdt.rs b/src/rgb/crdt.rs index e3879672..c9e50345 100644 --- a/src/rgb/crdt.rs +++ b/src/rgb/crdt.rs @@ -12,7 +12,7 @@ use std::{ use crate::rgb::{ structs::{RgbAccountV0, RgbAccountV1}, - swap::{PublicRgbOffers, RgbBidSwap}, + swap::{RgbAuctionSwaps, RgbBidSwap, RgbPublicSwaps}, }; #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] @@ -297,7 +297,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 +310,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..3160645d 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 { @@ -39,6 +43,8 @@ pub enum RgbPersistenceError { RetrieveSwapBids(String), // Retrieve Public Offers Error. {0} RetrievePublicOffers(String), + // Retrieve Auction Offers Error. {0} + RetrieveAuctionOffers(String), // Store Stock Error. {0} WriteStock(String), // Store RgbAccountV1 Error. {0} @@ -53,6 +59,8 @@ pub enum RgbPersistenceError { WriteRgbBids(String), // Store Public Offers Error. {0} WriteRgbPublicOffers(String), + // Store Auction Offers Error. {0} + WriteRgbAuctionOffers(String), // Store Swap Bid Error. {0} WriteSwapBids(String), } @@ -97,6 +105,17 @@ pub async fn retrieve_public_offers() -> Result Result { + let stock = retrieve_rgb_auctions_offers(bundle_id, name) + .await + .map_err(|op| RgbPersistenceError::RetrieveAuctionOffers(op.to_string()))?; + + Ok(stock) +} + pub async fn retrieve_swap_offer_bid( sk: &str, name: &str, @@ -203,6 +222,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::WriteRgbAuctionOffers(op.to_string())) +} + pub async fn store_stock_account( sk: &str, stock: Stock, diff --git a/src/rgb/prebuild.rs b/src/rgb/prebuild.rs index ac948f73..ae000ae9 100644 --- a/src/rgb/prebuild.rs +++ b/src/rgb/prebuild.rs @@ -27,7 +27,7 @@ use crate::{ use crate::rgb::{ constants::{BITCOIN_DEFAULT_FETCH_LIMIT, RGB_DEFAULT_FETCH_LIMIT}, - contract::export_contract, + contract::{export_boilerplate, export_contract}, fs::RgbPersistenceError, prefetch::prefetch_resolver_txs, prefetch::{ @@ -35,8 +35,8 @@ use crate::rgb::{ }, psbt::estimate_fee, resolvers::ExplorerResolver, - structs::AddressAmount, - structs::RgbExtractTransfer, + structs::{AddressAmount, ContractAmount, RgbExtractTransfer}, + swap::RgbSwapStrategy, swap::{get_public_offer, RgbBid, RgbOfferSwap}, transfer::extract_transfer, wallet::sync_wallet, @@ -44,8 +44,6 @@ use crate::rgb::{ RgbSwapError, SaveTransferError, TransferError, }; -use super::{contract::export_boilerplate, structs::ContractAmount}; - pub const DUST_LIMIT_SATOSHI: u64 = 546; pub async fn prebuild_transfer_asset( @@ -423,6 +421,7 @@ pub async fn prebuild_seller_swap( iface: iface_name, contract_amount: target_amount, bitcoin_changes, + strategy, .. } = request; @@ -513,15 +512,13 @@ pub async fn prebuild_seller_swap( let mut assets_allocs = vec![]; let mut total_asset_bitcoin_unspend: u64 = 0; - for alloc in allocations.iter() { - // // TODO: Make more tests! - // let sig_hash = if assets_inputs.len() <= 0 { - // PsbtSigHashRequest::NonePlusAnyoneCanPay - // } else { - // PsbtSigHashRequest::NonePlusAnyoneCanPay - // }; - let sig_hash = PsbtSigHashRequest::NonePlusAnyoneCanPay; + let asset_sig_hash = match strategy { + RgbSwapStrategy::Auction => PsbtSigHashRequest::NonePlusAnyoneCanPay, + RgbSwapStrategy::P2P | RgbSwapStrategy::HotSwap => PsbtSigHashRequest::NonePlusAnyoneCanPay, + }; + + for alloc in allocations.iter() { match alloc.value { AllocationValue::Value(alloc_value) => { if asset_total >= target_amount { @@ -532,7 +529,7 @@ pub async fn prebuild_seller_swap( descriptor: universal_desc.clone(), utxo: alloc.utxo.clone(), utxo_terminal: alloc.derivation.to_string(), - sigh_hash: Some(sig_hash), + sigh_hash: Some(asset_sig_hash.clone()), tapret: None, }; if !assets_inputs @@ -540,10 +537,6 @@ pub async fn prebuild_seller_swap( .into_iter() .any(|x: PsbtInputRequest| x.utxo == alloc.utxo) { - // let mut empty_input = input.clone(); - // empty_input.sigh_hash = Some(PsbtSigHashRequest::None); - - // assets_inputs.push(empty_input); assets_inputs.push(input); assets_allocs.push(alloc.clone()); total_asset_bitcoin_unspend += asset_unspent_utxos @@ -564,7 +557,7 @@ pub async fn prebuild_seller_swap( descriptor: universal_desc.clone(), utxo: alloc.utxo.clone(), utxo_terminal: alloc.derivation.to_string(), - sigh_hash: Some(sig_hash), + sigh_hash: Some(asset_sig_hash), tapret: None, }; if !assets_inputs @@ -590,70 +583,80 @@ pub async fn prebuild_seller_swap( } } - // Get All Bitcoin UTXOs - let total_bitcoin_spend: u64 = bitcoin_changes - .clone() - .into_iter() - .map(|x| { - let recipient = AddressAmount::from_str(&x).expect("invalid address amount format"); - recipient.amount - }) - .sum(); - let mut bitcoin_inputs = vec![]; - - let bitcoin_indexes = [0, 1]; - for bitcoin_index in bitcoin_indexes { - sync_wallet(bitcoin_index, rgb_wallet, resolver); - prefetch_resolver_utxos( - bitcoin_index, - rgb_wallet, - resolver, - Some(BITCOIN_DEFAULT_FETCH_LIMIT), - ) - .await; - prefetch_resolver_user_utxo_status(bitcoin_index, rgb_wallet, resolver, false).await; - - let mut unspent_utxos = - next_utxos(bitcoin_index, rgb_wallet.clone(), resolver).map_err(|_| { - RgbSwapError::IO(RgbPersistenceError::RetrieveRgbAccount("".to_string())) - })?; + let (bitcoin_inputs, change_value) = match strategy { + RgbSwapStrategy::P2P | RgbSwapStrategy::HotSwap => { + // Get All Bitcoin UTXOs + let total_bitcoin_spend: u64 = bitcoin_changes + .clone() + .into_iter() + .map(|x| { + let recipient = + AddressAmount::from_str(&x).expect("invalid address amount format"); + recipient.amount + }) + .sum(); + let mut bitcoin_inputs = vec![]; + + let bitcoin_indexes = [0, 1]; + for bitcoin_index in bitcoin_indexes { + sync_wallet(bitcoin_index, rgb_wallet, resolver); + prefetch_resolver_utxos( + bitcoin_index, + rgb_wallet, + resolver, + Some(BITCOIN_DEFAULT_FETCH_LIMIT), + ) + .await; + prefetch_resolver_user_utxo_status(bitcoin_index, rgb_wallet, resolver, false) + .await; + + let mut unspent_utxos = next_utxos(bitcoin_index, rgb_wallet.clone(), resolver) + .map_err(|_| { + RgbSwapError::IO(RgbPersistenceError::RetrieveRgbAccount("".to_string())) + })?; + + all_unspents.append(&mut unspent_utxos); + } - all_unspents.append(&mut unspent_utxos); - } + let mut bitcoin_total = total_asset_bitcoin_unspend; + let total_spendable = total_bitcoin_spend; - let mut bitcoin_total = total_asset_bitcoin_unspend; - let total_spendable = total_bitcoin_spend; + let bitcoin_sigh_hash = PsbtSigHashRequest::NonePlusAnyoneCanPay; + for utxo in all_unspents { + if bitcoin_total > total_spendable { + break; + } else { + let TerminalPath { app, index } = utxo.derivation.terminal; + let btc_input = PsbtInputRequest { + descriptor: universal_desc.clone(), + utxo: utxo.outpoint.to_string(), + utxo_terminal: format!("/{app}/{index}"), + sigh_hash: Some(bitcoin_sigh_hash.clone()), + tapret: None, + }; + if !bitcoin_inputs + .clone() + .into_iter() + .any(|x: PsbtInputRequest| x.utxo == utxo.outpoint.to_string()) + { + bitcoin_inputs.push(btc_input); + bitcoin_total += utxo.amount; + } + } + } - for utxo in all_unspents { - if bitcoin_total > total_spendable { - break; - } else { - let TerminalPath { app, index } = utxo.derivation.terminal; - let btc_input = PsbtInputRequest { - descriptor: universal_desc.clone(), - utxo: utxo.outpoint.to_string(), - utxo_terminal: format!("/{app}/{index}"), - sigh_hash: Some(PsbtSigHashRequest::NonePlusAnyoneCanPay), - tapret: None, - }; - if !bitcoin_inputs - .clone() - .into_iter() - .any(|x: PsbtInputRequest| x.utxo == utxo.outpoint.to_string()) - { - bitcoin_inputs.push(btc_input); - bitcoin_total += utxo.amount; + let change_value = bitcoin_total - total_spendable; + if bitcoin_total < total_spendable { + return Err(RgbSwapError::Inflation { + input: bitcoin_total, + output: total_spendable, + }); } - } - } - let change_value = bitcoin_total - total_spendable; - if bitcoin_total < total_spendable { - return Err(RgbSwapError::Inflation { - input: bitcoin_total, - output: total_spendable, - }); - } + (bitcoin_inputs, change_value) + } + RgbSwapStrategy::Auction => (vec![], total_asset_bitcoin_unspend), + }; Ok(( assets_allocs, diff --git a/src/rgb/prefetch.rs b/src/rgb/prefetch.rs index 090b6dc7..39525501 100644 --- a/src/rgb/prefetch.rs +++ b/src/rgb/prefetch.rs @@ -31,8 +31,8 @@ use std::{collections::BTreeMap, str::FromStr}; use strict_encoding::StrictDeserialize; use wallet::onchain::ResolveTx; -use super::resolvers::ExploreClientExtError; -use super::structs::MediaMetadata; +use crate::rgb::resolvers::ExploreClientExtError; +use crate::rgb::structs::MediaMetadata; #[cfg(not(target_arch = "wasm32"))] pub async fn prefetch_resolver_rgb( diff --git a/src/rgb/psbt.rs b/src/rgb/psbt.rs index b28ac354..b92a0b01 100644 --- a/src/rgb/psbt.rs +++ b/src/rgb/psbt.rs @@ -262,7 +262,7 @@ pub fn extract_output_commit(psbt: Psbt) -> Result<(Outpoint, u64, Vec), Dbc } } -pub fn save_tap_commit_str( +pub fn save_rgb_commit_str( outpoint: &str, amount: u64, commit: &str, @@ -278,10 +278,10 @@ pub fn save_tap_commit_str( let commit = Vec::::from_hex(commit).expect("invalid tap commit parse"); - save_tap_commit(outpoint, amount, commit, terminal, wallet); + save_rgb_commit(outpoint, amount, commit, terminal, wallet); } -pub fn save_tap_commit( +pub fn save_rgb_commit( outpoint: Outpoint, amount: u64, commit: Vec, @@ -313,7 +313,7 @@ pub fn save_tap_commit( wallet.utxos.insert(Utxo { amount, outpoint, - status: MiningStatus::Mempool, + status: MiningStatus::Blockchain(666), derivation: DeriveInfo::with(terminal.app, terminal.index, Some(tap_commit.clone())), }); wallet.descr = RgbDescr::Tapret(tapret); diff --git a/src/rgb/swap.rs b/src/rgb/swap.rs index f05c7467..e13f56f4 100644 --- a/src/rgb/swap.rs +++ b/src/rgb/swap.rs @@ -1,13 +1,4 @@ #![allow(deprecated)] -use super::{ - constants::LIB_NAME_BITMASK, - crdt::{LocalRgbOfferBid, LocalRgbOffers}, - fs::{ - retrieve_public_offers, retrieve_swap_offer_bid, store_public_offers, store_swap_bids, - RgbPersistenceError, - }, -}; -use crate::{structs::AllocationDetail, validators::RGBContext}; use amplify::{ confinement::{Confined, U32}, hex::{FromHex, ToHex}, @@ -15,7 +6,7 @@ use amplify::{ }; use autosurgeon::{reconcile, Hydrate, Reconcile}; use baid58::{Baid58ParseError, FromBaid58, ToBaid58}; -use bitcoin::psbt::Psbt; +use bitcoin::psbt::Psbt as PsbtV0; use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, Secp256k1, SecretKey}; use bitcoin_scripts::address::AddressCompat; use bp::Txid; @@ -37,6 +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::{structs::AllocationDetail, validators::RGBContext}; + type AssetId = String; type OfferId = String; type BidId = String; @@ -65,6 +66,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}")] @@ -73,11 +106,15 @@ pub struct RgbOffer { #[garde(length(min = 0, max = 100))] pub offer_id: OfferId, #[garde(skip)] - pub offer_status: RgbOrderStatus, + pub status: RgbOrderStatus, #[garde(ascii)] pub contract_id: AssetId, #[garde(ascii)] pub iface: String, + #[garde(skip)] + pub strategy: RgbSwapStrategy, + #[garde(ascii)] + pub pub_key: String, #[garde(ascii)] pub terminal: String, #[garde(range(min = u64::MIN, max = u64::MAX))] @@ -87,52 +124,48 @@ pub struct RgbOffer { #[garde(range(min = u64::MIN, max = u64::MAX))] pub bitcoin_price: u64, #[garde(ascii)] - pub seller_psbt: String, - #[garde(ascii)] pub seller_address: String, + #[garde(ascii)] + pub seller_psbt: String, #[garde(skip)] pub expire_at: Option, - #[garde(ascii)] - pub public: String, #[garde(skip)] - pub presig: bool, + pub bundle_id: Option, #[garde(skip)] pub transfer_id: Option, } impl RgbOffer { + pub fn is_fill(self) -> bool { + self.status == RgbOrderStatus::Fill + } + #[allow(clippy::too_many_arguments)] pub(crate) fn new( secret: String, 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 pub_key = PublicKey::from_secret_key(&secp, &secret_key).to_hex(); let mut asset_utxos: Vec = allocations.into_iter().map(|a| a.utxo).collect(); asset_utxos.sort(); let mut hasher = blake3::Hasher::new(); + hasher.update(contract_id.as_bytes()); for asset_utxo in asset_utxos { hasher.update(asset_utxo.as_bytes()); } @@ -143,7 +176,7 @@ impl RgbOffer { RgbOffer { offer_id: order_id.to_string(), - offer_status: RgbOrderStatus::Open, + status: RgbOrderStatus::Open, contract_id, iface, asset_amount, @@ -151,10 +184,11 @@ impl RgbOffer { bitcoin_price, seller_psbt: psbt, seller_address: seller_address.to_string(), - public: public_key.to_hex(), - presig, + pub_key, expire_at, terminal, + strategy, + bundle_id, ..Default::default() } } @@ -178,15 +212,17 @@ pub struct RgbOfferSwap { #[garde(range(min = u64::MIN, max = u64::MAX))] pub bitcoin_price: u64, #[garde(ascii)] - pub seller_psbt: String, - #[garde(ascii)] pub seller_address: String, #[garde(skip)] - pub expire_at: Option, + pub strategy: RgbSwapStrategy, #[garde(ascii)] - pub public: String, + pub pub_key: String, + #[garde(ascii)] + pub seller_psbt: Option, + #[garde(skip)] + pub bundle_id: Option, #[garde(skip)] - pub presig: bool, + pub expire_at: Option, } impl From for RgbOfferSwap { @@ -199,25 +235,33 @@ impl From for RgbOfferSwap { bitcoin_price, seller_psbt, seller_address, - public, - expire_at, - presig, + pub_key, asset_precision, + strategy, + expire_at, + bundle_id, .. } = value; + let seller_psbt = if seller_psbt.is_empty() { + None + } else { + Some(seller_psbt) + }; + Self { offer_id, + strategy, contract_id, iface, asset_amount, + asset_precision, bitcoin_price, seller_psbt, seller_address, - public, + pub_key, + bundle_id, expire_at, - presig, - asset_precision, } } } @@ -230,10 +274,12 @@ pub struct RgbBid { #[garde(length(min = 0, max = 100))] pub bid_id: BidId, #[garde(skip)] - pub bid_status: RgbOrderStatus, + pub status: RgbOrderStatus, #[garde(ascii)] #[garde(length(min = 0, max = 100))] pub offer_id: OfferId, + #[garde(ascii)] + pub pub_key: String, #[garde(skip)] pub contract_id: AssetId, #[garde(skip)] @@ -245,11 +291,9 @@ pub struct RgbBid { #[garde(range(min = u64::MIN, max = u64::MAX))] pub bitcoin_amount: u64, #[garde(ascii)] - pub buyer_psbt: String, - #[garde(ascii)] pub buyer_invoice: String, #[garde(ascii)] - pub public: String, + pub buyer_psbt: Option, #[garde(skip)] pub transfer_id: Option, #[garde(skip)] @@ -259,6 +303,10 @@ pub struct RgbBid { } impl RgbBid { + pub fn is_fill(self) -> bool { + self.status == RgbOrderStatus::Fill + } + pub(crate) fn new( secret: String, offer_id: OfferId, @@ -271,12 +319,13 @@ impl RgbBid { let secp = Secp256k1::new(); let secret = hex::decode(secret).expect("cannot decode hex sk in new RgbBid"); let secret_key = SecretKey::from_slice(&secret).expect("error parsing sk in new RgbBid"); - let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let pub_key = PublicKey::from_secret_key(&secp, &secret_key).to_hex(); let mut allocations = bitcoin_utxos; allocations.sort(); let mut hasher = blake3::Hasher::new(); + hasher.update(contract_id.as_bytes()); for allocation in allocations { hasher.update(allocation.as_bytes()); } @@ -287,13 +336,13 @@ impl RgbBid { RgbBid { bid_id: order_id.to_string(), - bid_status: RgbOrderStatus::Open, + status: RgbOrderStatus::Open, offer_id, contract_id, asset_amount, asset_precision, bitcoin_amount: bitcoin_price, - public: public_key.to_hex(), + pub_key, ..Default::default() } } @@ -310,6 +359,8 @@ pub struct RgbBidSwap { #[garde(length(min = 0, max = 100))] pub offer_id: OfferId, #[garde(ascii)] + pub pub_key: String, + #[garde(ascii)] #[garde(length(min = 0, max = 100))] pub iface: String, #[garde(skip)] @@ -321,11 +372,9 @@ pub struct RgbBidSwap { #[garde(range(min = u64::MIN, max = u64::MAX))] pub bitcoin_amount: u64, #[garde(ascii)] - pub buyer_psbt: String, - #[garde(ascii)] pub buyer_invoice: String, - #[garde(ascii)] - pub public: String, + #[garde(skip)] + pub buyer_psbt: Option, #[garde(skip)] pub transfer_id: Option, #[garde(skip)] @@ -333,11 +382,11 @@ pub struct RgbBidSwap { #[garde(skip)] pub swap_psbt: Option, #[garde(skip)] - pub tap_outpoint: Option, + pub swap_outpoint: Option, #[garde(skip)] - pub tap_amount: Option, + pub swap_amount: Option, #[garde(skip)] - pub tap_commit: Option, + pub swap_commit: Option, } impl From for RgbBidSwap { @@ -351,7 +400,7 @@ impl From for RgbBidSwap { bitcoin_amount, buyer_psbt, buyer_invoice, - public, + pub_key, transfer_id, transfer, iface, @@ -369,7 +418,7 @@ impl From for RgbBidSwap { bitcoin_amount, buyer_psbt, buyer_invoice, - public, + pub_key, transfer_id, transfer, swap_psbt, @@ -390,7 +439,7 @@ pub struct PublicRgbBid { #[garde(range(min = u64::MIN, max = u64::MAX))] pub bitcoin_amount: u64, #[garde(ascii)] - pub public: String, + pub pub_key: String, } impl From for PublicRgbBid { @@ -399,7 +448,7 @@ impl From for PublicRgbBid { bid_id, asset_amount, bitcoin_amount, - public, + pub_key, .. } = value; @@ -407,7 +456,7 @@ impl From for PublicRgbBid { bid_id, asset_amount, bitcoin_amount, - public, + pub_key, } } } @@ -418,17 +467,192 @@ pub struct RgbOffers { pub bids: BTreeMap>, } +impl RgbOffers { + pub fn get_offers(self, bundle_id: String) -> Vec { + let mut item = vec![]; + for offers in self.offers.values() { + let offers = offers.to_vec(); + + item.extend( + offers + .into_iter() + .filter(|x| x.bundle_id.clone().unwrap_or_default() == bundle_id), + ); + } + item + } + + pub fn get_offer(self, offer_id: OfferId) -> Option { + let mut item = None; + for offers in self.offers.values() { + if let Some(offer) = offers.iter().find(|x| x.offer_id == offer_id) { + item = Some(offer.to_owned()); + break; + } + } + item + } + + 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 get_offer(self, bundle_id: String, offer_id: String) -> Option { + self.items.into_iter().find(|x| { + x.bundle_id.clone().unwrap_or_default() == bundle_id && x.offer_id == offer_id + }) + } + + pub fn get_offers(self, bundle_id: String) -> Vec { + self.items + .into_iter() + .filter(|x| x.bundle_id.clone().unwrap_or_default() == bundle_id) + .collect() + } + + 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 { + self.bundle_id = offer.bundle_id.clone().unwrap_or_default(); + 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 +664,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 +698,24 @@ 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(rgb_offers.get_offer(bundle_id.to_owned(), offer_id.clone())) +} + 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 +731,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 +741,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) { @@ -501,7 +751,11 @@ pub async fn get_swap_bids_by_seller( let mut swap_bids = vec![]; for bid in public_bids { - let PublicRgbBid { bid_id, public, .. } = bid.clone(); + let PublicRgbBid { + bid_id, + pub_key: public, + .. + } = bid.clone(); let secret = hex::decode(sk).map_err(|op| RgbOfferErrors::Keys(op.to_string()))?; let secret_key = SecretKey::from_slice(&secret).map_err(|op| RgbOfferErrors::Keys(op.to_string()))?; @@ -521,7 +775,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, @@ -533,7 +787,7 @@ pub async fn get_swap_bid( let secret_key = SecretKey::from_slice(&secret).map_err(|op| RgbOfferErrors::Keys(op.to_string()))?; let public_key = - PublicKey::from_str(&bid.public).map_err(|op| RgbOfferErrors::Keys(op.to_string()))?; + PublicKey::from_str(&bid.pub_key).map_err(|op| RgbOfferErrors::Keys(op.to_string()))?; let share_sk = SharedSecret::new(&public_key, &secret_key); let share_sk = share_sk.display_secret().to_string(); @@ -553,7 +807,9 @@ pub async fn get_swap_bid_by_buyer( bid_id: BidId, ) -> Result { let RgbOfferSwap { - expire_at, public, .. + expire_at, + pub_key: public, + .. } = get_public_offer(offer_id.clone()).await?; let secret = hex::decode(sk).map_err(|op| RgbOfferErrors::Keys(op.to_string()))?; @@ -574,40 +830,93 @@ pub async fn get_swap_bid_by_buyer( Ok(rgb_bid) } +pub async fn get_auction_highest_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 highest_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.iter().max_by_key(|x| x.bitcoin_amount).cloned() { + highest_bids.push(bid); + } + }; + } + + Ok(highest_bids) +} + +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() + .get_offer(bundle_id.to_owned(), offer_id.clone()) + .is_none() + { + 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); + + reconcile(&mut current_version, rgb_offers) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_public_offers(current_version.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(()) +} + +pub async fn publish_public_offers(new_offers: Vec) -> Result<(), RgbOfferErrors> { + let LocalRgbOffers { + mut rgb_offers, + version, + } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; + + let mut current_version = automerge::AutoCommit::load(&version) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + for new_offer in new_offers { + 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)?; @@ -615,31 +924,18 @@ pub async fn publish_public_offer(new_offer: RgbOfferSwap) -> Result<(), RgbOffe } 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()))?; @@ -668,12 +964,15 @@ 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 mut local_copy = automerge::AutoCommit::load(&doc) + 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(&version) .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; reconcile(&mut local_copy, new_bid).map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; @@ -685,13 +984,67 @@ pub async fn publish_swap_bid( 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.clone()); + reconcile(&mut current_version, rgb_offers.clone()) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_auction_offers(&bundle_id, &file_name, current_version.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + publish_public_offers(new_offers).await?; + 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)?; + + Ok(()) +} + 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(); @@ -721,7 +1074,7 @@ pub async fn remove_public_offers(offers: Vec) -> Result<(), RgbOfferE Ok(()) } -pub async fn mark_transfer_offer( +pub async fn update_transfer_offer( offer_id: OfferId, consig_id: TransferId, rgb_offers: &mut RgbOffers, @@ -740,7 +1093,7 @@ pub async fn mark_transfer_offer( Ok(()) } -pub async fn mark_transfer_bid( +pub async fn update_transfer_bid( bid_id: BidId, consig_id: TransferId, rgb_bids: &mut RgbBids, @@ -750,7 +1103,6 @@ pub async fn mark_transfer_bid( if let Some(position) = my_bids.iter().position(|x| x.bid_id == bid_id) { let mut offer = my_bids.swap_remove(position); offer.transfer_id = Some(consig_id.to_owned()); - // offer.transfer = transfer; my_bids.insert(position, offer); rgb_bids.bids.insert(contract_id, my_bids); @@ -760,7 +1112,7 @@ pub async fn mark_transfer_bid( Ok(()) } -pub async fn mark_offer_fill( +pub async fn complete_offer( transfer_id: TransferId, rgb_offers: &mut RgbOffers, ) -> Result, RgbOfferErrors> { @@ -773,7 +1125,7 @@ pub async fn mark_offer_fill( .position(|x| x.transfer_id.unwrap_or_default() == transfer_id) { let mut offer = my_offers.swap_remove(position); - offer.offer_status = RgbOrderStatus::Fill; + offer.status = RgbOrderStatus::Fill; offer_filled = Some(offer.clone()); my_offers.insert(position, offer); @@ -785,7 +1137,7 @@ pub async fn mark_offer_fill( Ok(offer_filled) } -pub async fn mark_bid_fill( +pub async fn complete_bid( transfer_id: TransferId, rgb_bids: &mut RgbBids, ) -> Result, RgbOfferErrors> { @@ -798,7 +1150,7 @@ pub async fn mark_bid_fill( .position(|x| x.transfer_id.unwrap_or_default() == transfer_id) { let mut bid = my_bids.swap_remove(position); - bid.bid_status = RgbOrderStatus::Fill; + bid.status = RgbOrderStatus::Fill; bid_filled = Some(bid.clone()); my_bids.insert(position, bid); @@ -827,15 +1179,15 @@ pub trait PsbtSwapEx { fn join(self, other: T) -> Result; } -impl PsbtSwapEx for Psbt { +impl PsbtSwapEx for PsbtV0 { type Error = PsbtSwapExError; - fn join(self, other: Psbt) -> Result { + fn join(self, other: PsbtV0) -> Result { // BIP 174: The Combiner must remove any duplicate key-value pairs, in accordance with // the specification. It can pick arbitrarily when conflicts occur. // Keeping the highest version - let mut new_psbt = Psbt::from(self).clone(); + let mut new_psbt = PsbtV0::from(self).clone(); // let mut other = other; new_psbt.version = cmp::max(new_psbt.version, other.version); @@ -877,10 +1229,17 @@ impl PsbtSwapEx for Psbt { new_psbt.proprietary.extend(other.proprietary); new_psbt.unknown.extend(other.unknown); - // TODO: Make more tests! - // new_psbt.inputs.remove(0); - // new_psbt.inputs.insert(0, other.inputs.remove(0)); - new_psbt.inputs.extend(other.inputs); + // new_psbt.inputs.extend(other.inputs); + let current_inputs = new_psbt.inputs.clone(); + let new_inputs = other.inputs.clone(); + new_inputs.into_iter().for_each(|vin| { + if !current_inputs.clone().into_iter().any(|x| { + x.bip32_derivation == vin.bip32_derivation + && x.non_witness_utxo.eq(&vin.non_witness_utxo) + }) { + new_psbt.inputs.push(vin); + } + }); let current_outputs = new_psbt.outputs.clone(); let new_outputs = other.outputs.clone(); @@ -901,12 +1260,18 @@ impl PsbtSwapEx for Psbt { new_psbt.unsigned_tx.lock_time = cmp::max(new_psbt.unsigned_tx.lock_time, other.unsigned_tx.lock_time); - // new_psbt.unsigned_tx.input.remove(0); - // new_psbt - // .unsigned_tx - // .input - // .insert(0, other.unsigned_tx.input.remove(0)); - new_psbt.unsigned_tx.input.extend(other.unsigned_tx.input); + // new_psbt.unsigned_tx.input.extend(other.unsigned_tx.input); + let current_inputs = new_psbt.unsigned_tx.input.clone(); + let new_inputs = other.unsigned_tx.input.clone(); + new_inputs.into_iter().for_each(|vin| { + if !current_inputs + .clone() + .into_iter() + .any(|x| x.previous_output.eq(&vin.previous_output)) + { + new_psbt.unsigned_tx.input.push(vin); + } + }); let current_outputs = new_psbt.unsigned_tx.output.clone(); let new_outputs = other.unsigned_tx.output.clone(); diff --git a/src/rgb/transfer.rs b/src/rgb/transfer.rs index d3cda793..5267bd6e 100644 --- a/src/rgb/transfer.rs +++ b/src/rgb/transfer.rs @@ -154,7 +154,7 @@ pub fn pay_invoice( PSBT::deserialize(&psbt).map_err(|err| NewPaymentError::WrongPSBT(err.to_string()))?; let transfers = stock - .process(invoice, &mut psbt_final, CloseMethod::TapretFirst, options) + .pay_all(invoice, &mut psbt_final, CloseMethod::TapretFirst, options) .map_err(|err| NewPaymentError::NoPay(err.to_string()))?; let psbt_file = Psbt::from_str(&PSBT::serialize(&psbt_final).to_hex()) diff --git a/src/structs.rs b/src/structs.rs index 3be0efd3..c40baeb0 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, @@ -42,6 +42,14 @@ pub struct WalletTransaction { pub confirmation_time: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TransactionData { + pub details: TransactionDetails, + pub vsize: usize, + pub fee_rate: f32, +} + #[derive(Serialize, Deserialize, Clone, Debug, Zeroize, ZeroizeOnDrop, Display, Default)] #[display(inner)] pub struct SecretString(pub String); @@ -1344,6 +1352,19 @@ impl UtxoSpentStatus { } } +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] +#[garde(context(RGBContext))] +#[serde(rename_all = "camelCase")] +#[display(doc_comments)] +pub struct RgbAuctionOfferRequest { + #[garde(skip)] + pub sign_keys: Vec, + + /// List of Offers + #[garde(dive)] + pub offers: Vec, +} + #[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] #[garde(context(RGBContext))] #[serde(rename_all = "camelCase")] @@ -1373,7 +1394,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, } @@ -1394,6 +1415,8 @@ pub struct RgbOfferResponse { pub seller_address: String, /// Seller PSBT (encoded in base64) pub seller_psbt: String, + /// Bundle ID (collection) + pub bundle_id: Option, } #[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] @@ -1449,6 +1472,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}")] @@ -1465,6 +1508,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")] @@ -1495,6 +1576,81 @@ pub struct RgbSwapResponse { pub final_psbt: String, } +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{consig_id}")] +pub struct RgbMatchResponse { + /// 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("{offer_id} ~ {contract_id}:{asset_amount} = {bitcoin_price}")] +pub struct RgbAuctionOfferResponse { + /// Offer ID + pub offer_id: String, + /// Contract ID + pub contract_id: String, + /// Asset/Contract Amount + pub asset_amount: u64, + /// Bitcoin Price + pub bitcoin_price: u64, + /// Bundle ID + pub bundle_id: String, +} + +impl From for RgbAuctionOfferResponse { + fn from(value: RgbOfferSwap) -> Self { + let RgbOfferSwap { + offer_id, + contract_id, + asset_amount, + bitcoin_price, + bundle_id, + .. + } = value; + + Self { + offer_id, + contract_id, + asset_amount, + bitcoin_price, + bundle_id: bundle_id.unwrap_or_default(), + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{bundle_id}")] +pub struct RgbAuctionFinishResponse { + /// Bundle ID + pub bundle_id: String, + /// New Change Outpoint + pub outpoint: String, + /// Sold Items + pub sold: BTreeMap, + /// Reamining Items + pub remaining: BTreeMap, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{contract_id}:{contract_amount}")] +pub struct RgbSwapItem { + /// Contract ID + pub contract_id: String, + /// Iface + pub iface: String, + /// Final Consig + pub contract_amount: String, +} + #[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] #[serde(rename_all = "camelCase")] #[display("{offers:?}")] @@ -1521,7 +1677,7 @@ pub struct PublicRgbOfferResponse { /// Bitcoin Price bitcoin_price: u64, /// Initial Offer PSBT - offer_psbt: String, + offer_psbt: Option, } impl From for PublicRgbOfferResponse { @@ -1531,7 +1687,7 @@ impl From for PublicRgbOfferResponse { offer_id: value.offer_id, asset_amount: value.asset_amount, bitcoin_price: value.bitcoin_price, - offer_pub: value.public, + offer_pub: value.pub_key, offer_psbt: value.seller_psbt, } } @@ -1610,7 +1766,7 @@ impl From for RgbOfferDetail { Self { contract_id: value.contract_id, offer_id: value.offer_id, - offer_status: value.offer_status.to_string(), + offer_status: value.status.to_string(), asset_amount: value.asset_amount, bitcoin_price: value.bitcoin_price, } @@ -1641,7 +1797,7 @@ impl From for RgbBidDetail { contract_id: value.contract_id, offer_id: value.offer_id, bid_id: value.bid_id, - bid_status: value.bid_status.to_string(), + bid_status: value.status.to_string(), asset_amount: value.asset_amount, bitcoin_price: value.bitcoin_amount, } diff --git a/src/web.rs b/src/web.rs index 0ebdfb93..bd00c8f0 100644 --- a/src/web.rs +++ b/src/web.rs @@ -9,8 +9,9 @@ use crate::rgb::structs::ContractAmount; use crate::structs::{ AcceptRequest, FullIssueRequest, FullRgbTransferRequest, ImportRequest, InvoiceRequest, IssueMediaRequest, IssueRequest, MediaRequest, PsbtRequest, PublishPsbtRequest, ReIssueRequest, - RgbBidRequest, RgbOfferRequest, RgbRemoveTransferRequest, RgbSaveTransferRequest, - RgbSwapRequest, RgbTransferRequest, SecretString, SignPsbtRequest, WatcherRequest, + RgbAuctionBidRequest, RgbBidRequest, RgbOfferRequest, RgbRemoveTransferRequest, + RgbSaveTransferRequest, RgbSwapRequest, RgbTransferRequest, SecretString, SignPsbtRequest, + WatcherRequest, }; pub fn set_panic_hook() { @@ -902,6 +903,21 @@ pub mod rgb { }) } + #[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(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } + #[wasm_bindgen] pub fn create_bid(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); @@ -932,6 +948,34 @@ pub mod rgb { }) } + #[wasm_bindgen] + pub fn finish_auction(nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let bundle_id: String = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::finish_auction_offers(&nostr_hex_sk, bundle_id).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 list_auctions() -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::rgb::list_auctions().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 direct_swap(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); diff --git a/tests/_init.rs b/tests/_init.rs new file mode 100644 index 00000000..04f403d3 --- /dev/null +++ b/tests/_init.rs @@ -0,0 +1,11 @@ +#![cfg(not(target_arch = "wasm32"))] + +use anyhow::Result; +use bitmask_core::regtest::init_fs; + +#[test] +pub fn _init() -> Result<()> { + init_fs()?; + + Ok(()) +} diff --git a/tests/rgb.rs b/tests/rgb.rs index dd323ae8..0f8d71b0 100644 --- a/tests/rgb.rs +++ b/tests/rgb.rs @@ -26,6 +26,7 @@ mod rgb { mod issue; mod proxy; mod rbf; + mod sign_hash; mod states; mod swaps; mod transfers; diff --git a/tests/rgb/integration/drain.rs b/tests/rgb/integration/drain.rs index 305c37ac..b5626197 100644 --- a/tests/rgb/integration/drain.rs +++ b/tests/rgb/integration/drain.rs @@ -49,11 +49,11 @@ pub async fn drain() -> Result<()> { .await?; assert_eq!( - drain_wallet_details.received, 0, + drain_wallet_details.details.received, 0, "received no funds in this transaction" ); assert_eq!( - drain_wallet_details.sent + drain_wallet_details.fee.expect("fee present"), + drain_wallet_details.details.sent + drain_wallet_details.details.fee.expect("fee present"), 30_000_000, "received 0.3 tBTC" ); diff --git a/tests/rgb/integration/sign_hash.rs b/tests/rgb/integration/sign_hash.rs new file mode 100644 index 00000000..aa7230fc --- /dev/null +++ b/tests/rgb/integration/sign_hash.rs @@ -0,0 +1,307 @@ +#![cfg(not(target_arch = "wasm32"))] +use amplify::hex::ToHex; +use bdk::wallet::{AddressIndex, AddressInfo}; +use bitcoin::{psbt::PartiallySignedTransaction as PsbtV0, EcdsaSighashType}; +use bitcoin_blockchain::locks::SeqNo; +use bitmask_core::{ + bitcoin::{ + get_new_address, get_wallet, new_mnemonic, publish_psbt_file, sign_psbt_file, sync_wallet, + }, + constants::BITCOIN_EXPLORER_API, + rgb::{ + create_watcher, + psbt::{NewPsbtOptions, PsbtEx}, + resolvers::ExplorerResolver, + swap::PsbtSwapEx, + }, + structs::{ + PrivateWalletData, PublicWalletData, PublishPsbtRequest, SecretString, SignPsbtRequest, + SignedPsbtResponse, WatcherRequest, + }, +}; +use std::str::FromStr; + +use miniscript_crate::Descriptor; +use psbt::{serialize::Serialize, Psbt}; +use wallet::{ + descriptors::InputDescriptor, + hd::{DerivationAccount, DerivationSubpath, UnhardenedIndex}, +}; + +use crate::rgb::integration::utils::send_some_coins; + +#[tokio::test] +async fn create_auction_signatures() -> anyhow::Result<()> { + // 1. Initial Setup + let bob_keys = new_mnemonic(&SecretString("".to_string())).await?; + let alice_keys = new_mnemonic(&SecretString("".to_string())).await?; + let charlie_keys = new_mnemonic(&SecretString("".to_string())).await?; + + for participant in [alice_keys.clone()] { + let watcher_name = "default"; + let participant_pubkey = participant.public.btc_descriptor_xpub.clone(); + let participant_sk = participant.private.nostr_prv.clone(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: participant.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&participant_sk, create_watch_req.clone()).await?; + + let participant_address = get_new_address(&SecretString(participant_pubkey), None).await?; + let default_coins = "0.00001000"; + + send_some_coins(&participant_address, default_coins).await; + } + + for participant in [bob_keys.clone(), charlie_keys.clone()] { + let watcher_name = "default"; + let participant_pubkey = participant.public.btc_descriptor_xpub.clone(); + let participant_sk = participant.private.nostr_prv.clone(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: participant.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&participant_sk, create_watch_req.clone()).await?; + + let participant_address = get_new_address(&SecretString(participant_pubkey), None).await?; + let default_coins = "0.01"; + + send_some_coins(&participant_address, default_coins).await; + } + + // 2. Alice Build PSBT Offers (aka. Seller) + let tx_resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..Default::default() + }; + + let PublicWalletData { + btc_descriptor_xpub: alice_pubkey, + .. + } = &alice_keys.public; + let PrivateWalletData { + btc_descriptor_xprv: alice_prv, + .. + } = &alice_keys.private; + let alice_descriptor = alice_pubkey.replace("/0/*", "/*/*"); + let alice_descriptor: &Descriptor = + &Descriptor::from_str(&alice_descriptor)?; + + let alice_prv = SecretString(alice_prv.to_owned()); + let alice_wallet = get_wallet(&alice_prv, None).await?; + sync_wallet(&alice_wallet).await?; + + let alice_terminal = "/0/0".parse::>()?; + let alice_utxos = alice_wallet.lock().await.list_unspent()?; + + let inputs: Vec = alice_utxos + .clone() + .into_iter() + .map(|x| InputDescriptor { + outpoint: x.outpoint, + terminal: alice_terminal.clone(), + seq_no: SeqNo::default(), + tweak: None, + sighash_type: EcdsaSighashType::NonePlusAnyoneCanPay, + }) + .collect(); + + let alice_wallet = get_wallet(&SecretString(alice_pubkey.to_owned()), None).await?; + sync_wallet(&alice_wallet).await?; + + let AddressInfo { + address: address_1, .. + } = alice_wallet.lock().await.get_address(AddressIndex::New)?; + + let bitcoin_fee = 0; + let change_index = "/1/0".parse::>()?; + let options = NewPsbtOptions::set_inflaction(1_000_u64); + + let alice_outputs = vec![(address_1.script_pubkey().into(), 1_000_u64)]; + let offer_1st = Psbt::new( + alice_descriptor, + &inputs, + &vec![], + change_index.to_vec(), + bitcoin_fee, + &tx_resolver, + options.clone(), + ) + .expect("invalid 1st offer psbt"); + + let offer_2nd = Psbt::new( + alice_descriptor, + &inputs, + &vec![], + change_index.to_vec(), + bitcoin_fee, + &tx_resolver, + options.clone(), + ) + .expect("invalid 1st offer psbt"); + + // 3. Bob Build One PSBT Bid (aka. Buyer) + let PublicWalletData { + btc_descriptor_xpub: bob_pubkey, + .. + } = &bob_keys.public; + let PrivateWalletData { + btc_descriptor_xprv: bob_prv, + .. + } = &bob_keys.private; + let bob_descriptor = bob_pubkey.replace("/0/*", "/*/*"); + let bob_descriptor: &Descriptor = &Descriptor::from_str(&bob_descriptor)?; + + let bob_prv = SecretString(bob_prv.to_owned()); + let bob_wallet = get_wallet(&bob_prv, None).await?; + sync_wallet(&bob_wallet).await?; + + let bob_terminal = "/0/0".parse::>()?; + let bob_utxos = bob_wallet.lock().await.list_unspent()?; + + let inputs: Vec = bob_utxos + .into_iter() + .map(|x| InputDescriptor { + outpoint: x.outpoint, + terminal: bob_terminal.clone(), + seq_no: SeqNo::default(), + tweak: None, + sighash_type: EcdsaSighashType::NonePlusAnyoneCanPay, + }) + .collect(); + + let change_index = "/1/0".parse::>()?; + let bitcoin_fee = 1_000; + + let options = NewPsbtOptions::default(); + + let bid_1st = Psbt::new( + bob_descriptor, + &inputs, + &alice_outputs, + change_index.to_vec(), + bitcoin_fee, + &tx_resolver, + options, + ) + .expect("invalid 1st bid psbt"); + + // 4. Charlie Build One PSBT Bid (aka. Buyer) + let PublicWalletData { + btc_descriptor_xpub: charlie_pubkey, + .. + } = &charlie_keys.public; + let PrivateWalletData { + btc_descriptor_xprv: charlie_prv, + .. + } = &charlie_keys.private; + let charlie_descriptor = charlie_pubkey.replace("/0/*", "/*/*"); + let charlie_descriptor: &Descriptor = + &Descriptor::from_str(&charlie_descriptor)?; + + let charlie_prv = SecretString(charlie_prv.to_owned()); + let charlie_wallet = get_wallet(&charlie_prv, None).await?; + sync_wallet(&charlie_wallet).await?; + + let charlie_terminal = "/0/0".parse::>()?; + let charlie_utxos = charlie_wallet.lock().await.list_unspent()?; + + let inputs: Vec = charlie_utxos + .into_iter() + .map(|x| InputDescriptor { + outpoint: x.outpoint, + terminal: charlie_terminal.clone(), + seq_no: SeqNo::default(), + tweak: None, + sighash_type: EcdsaSighashType::NonePlusAnyoneCanPay, + }) + .collect(); + + let change_index = "/1/0".parse::>()?; + let bitcoin_fee = 1_000; + + let options = NewPsbtOptions::default(); + + let bid_2nd = Psbt::new( + charlie_descriptor, + &inputs, + &alice_outputs, + change_index.to_vec(), + bitcoin_fee, + &tx_resolver, + options, + ) + .expect("invalid 1st bid psbt"); + + // 5. Create First Swap (Sign) + let sign_req = SignPsbtRequest { + psbt: offer_1st.to_string(), + descriptors: vec![alice_prv.clone()], + }; + let SignedPsbtResponse { psbt, .. } = sign_psbt_file(sign_req).await?; + let offer_1st = Psbt::from_str(&psbt)?; + + let sign_req = SignPsbtRequest { + psbt: bid_1st.to_string(), + descriptors: vec![bob_prv.clone()], + }; + let SignedPsbtResponse { psbt, .. } = sign_psbt_file(sign_req).await?; + let bid_1st = Psbt::from_str(&psbt)?; + + let offer_1st_v0 = PsbtV0::from(offer_1st); + let bid_1st_v0 = PsbtV0::from(bid_1st); + let swap_1st = offer_1st_v0.join(bid_1st_v0)?; + // let swap_1st = Psbt::from(swap_1st); + + // let swap_1st = Serialize::serialize(&swap_1st.clone()).to_hex(); + // let publish_req = PublishPsbtRequest { + // psbt: swap_1st.to_string(), + // }; + + // let publish_resp = publish_psbt_file(publish_req).await; + // assert!(publish_resp.is_ok()); + + // 6. Create Second Swap (Sign) + let sign_req = SignPsbtRequest { + psbt: offer_2nd.to_string(), + descriptors: vec![alice_prv.clone()], + }; + let SignedPsbtResponse { psbt, .. } = sign_psbt_file(sign_req).await?; + let offer_2nd = Psbt::from_str(&psbt)?; + + let sign_req = SignPsbtRequest { + psbt: bid_2nd.to_string(), + descriptors: vec![charlie_prv.clone()], + }; + let SignedPsbtResponse { psbt, .. } = sign_psbt_file(sign_req).await?; + let bid_2nd = Psbt::from_str(&psbt)?; + + let offer_2nd_v0 = PsbtV0::from(offer_2nd); + let bid_2nd_v0 = PsbtV0::from(bid_2nd); + let swap_2nd = offer_2nd_v0.join(bid_2nd_v0)?; + // let swap_2nd = Psbt::from(swap_2nd); + + // let swap_2nd = Serialize::serialize(&swap_2nd.clone()).to_hex(); + // let publish_req = PublishPsbtRequest { + // psbt: swap_2nd.to_string(), + // }; + + // let publish_resp = publish_psbt_file(publish_req).await; + // assert!(publish_resp.is_ok()); + + // 7. Create Final Swap (Publish) + let swap_final = swap_1st.join(swap_2nd)?; + let swap_final = Psbt::from(swap_final); + + let swap_final = Serialize::serialize(&swap_final.clone()).to_hex(); + let publish_req = PublishPsbtRequest { + psbt: swap_final.to_string(), + }; + + let publish_resp = publish_psbt_file(publish_req).await; + assert!(publish_resp.is_ok()); + + Ok(()) +} diff --git a/tests/rgb/integration/swaps.rs b/tests/rgb/integration/swaps.rs index baf81012..735fd929 100644 --- a/tests/rgb/integration/swaps.rs +++ b/tests/rgb/integration/swaps.rs @@ -1,6 +1,7 @@ +#![allow(unused_imports)] #![cfg(not(target_arch = "wasm32"))] use crate::rgb::integration::utils::{ - get_uda_data, issuer_issue_contract_v2, send_some_coins, UtxoFilter, + generate_new_block, get_uda_data, issuer_issue_contract_v2, send_some_coins, UtxoFilter, }; use bitmask_core::{ bitcoin::{ @@ -8,20 +9,22 @@ use bitmask_core::{ sign_and_publish_psbt_file, sign_psbt_file, sync_wallet, }, rgb::{ - accept_transfer, create_buyer_bid, create_seller_offer, create_swap_transfer, - create_watcher, get_contract, import as import_contract, structs::ContractAmount, + 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, update_seller_offer, verify_transfers, }, structs::{ AcceptRequest, AssetType, ImportRequest, IssueResponse, PsbtFeeRequest, PublishPsbtRequest, - RgbBidRequest, RgbBidResponse, RgbOfferRequest, RgbOfferResponse, RgbOfferUpdateRequest, - RgbSwapRequest, RgbSwapResponse, SecretString, SignPsbtRequest, SignedPsbtResponse, - WatcherRequest, + RgbAuctionBidRequest, RgbAuctionOfferRequest, RgbAuctionOfferResponse, RgbBidRequest, + RgbBidResponse, RgbOfferRequest, RgbOfferResponse, RgbOfferUpdateRequest, RgbSwapRequest, + RgbSwapResponse, SecretString, SignPsbtRequest, SignedPsbtResponse, WatcherRequest, }, + util::init_logging, }; #[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 +134,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 +149,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 +252,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 +377,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 +479,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 +605,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; @@ -717,3 +720,452 @@ async fn create_presig_scriptless_swap() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn create_auction_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( + 3, + "RGB20", + ContractAmount::with(2, 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 = "1.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: 1_000, + descriptor: desc, + change_terminal: "/20/1".to_string(), + bitcoin_changes: vec![], + strategy: RgbSwapStrategy::Auction, + expire_at: None, + }; + + offers_collection.push(req); + } + + let offer_auction_req = RgbAuctionOfferRequest { + 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); + + let RgbOfferResponse { + offer_id: offer_2nd, + contract_id: contract_2nd, + .. + } = 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(1000), + 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. Create Bid (2nd Offer) + let buyer_btc_desc = buyer_keys.public.btc_descriptor_xpub.clone(); + let bid_auction_req = RgbAuctionBidRequest { + offer_id: offer_2nd.clone(), + asset_amount: contract_amount.clone(), + descriptor: SecretString(buyer_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(1000), + 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!(1., resp?.balance_normalized); + + let resp = get_contract(&seller_sk, &contract_1st).await; + assert!(resp.is_ok()); + assert_eq!(1., resp?.balance_normalized); + + // // 12. Check Balances (2nd Offer) + let resp = get_contract(&buyer_sk, &contract_2nd).await; + assert!(resp.is_ok()); + // println!("{:#?}", resp?.allocations); + assert_eq!(1., resp?.balance_normalized); + + let resp = get_contract(&seller_sk, &contract_2nd).await; + assert!(resp.is_ok()); + // println!("{:#?}", resp?.allocations); + assert_eq!(1., resp?.balance_normalized); + + Ok(()) +} + +#[tokio::test] +async fn create_collectible_auction() -> anyhow::Result<()> { + init_logging("bitmask_core=debug"); + + // 1. Initial Setup + let alice_keys = new_mnemonic(&SecretString("".to_string())).await?; + let bob_keys = new_mnemonic(&SecretString("".to_string())).await?; + + let watcher_name = "default"; + let alice_sk = alice_keys.private.nostr_prv.clone(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: alice_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&alice_sk, create_watch_req.clone()).await?; + + let bob_sk = bob_keys.private.nostr_prv.clone(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: bob_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&bob_sk, create_watch_req.clone()).await?; + + // 2. Setup Wallets (Seller) + let btc_address_1 = get_new_address( + &SecretString(alice_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(alice_keys.private.btc_descriptor_xprv.clone()); + let btc_change_descriptor_xprv = + SecretString(alice_keys.private.btc_change_descriptor_xprv.clone()); + + let assets_address_1 = get_new_address( + &SecretString(alice_keys.public.rgb_assets_descriptor_xpub.clone()), + None, + ) + .await?; + + let uda_address_1 = get_new_address( + &SecretString(alice_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(bob_keys.public.btc_descriptor_xpub.clone()), + None, + ) + .await?; + let asset_address_1 = get_new_address( + &SecretString(bob_keys.public.rgb_udas_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 metadata = get_uda_data(); + let issuer_resp = issuer_issue_contract_v2( + 2, + "RGB21", + ContractAmount::with(1, 0, 0).to_value(), + false, + false, + Some(metadata), + None, + Some(UtxoFilter::with_outpoint( + fund_vault.udas_output.unwrap_or_default(), + )), + Some(alice_keys.clone()), + ) + .await?; + + for contract in issuer_resp.clone() { + let bob_import_req = ImportRequest { + import: AssetType::RGB20, + data: contract.contract.strict, + }; + let bob_import_resp = import_contract(&bob_sk, bob_import_req).await; + assert!(bob_import_resp.is_ok()); + } + + // 5. Create Collection (Seller) + let contract_amount = "1.00".to_string(); + let mut offers_collection = vec![]; + for contract in issuer_resp.clone() { + let IssueResponse { + contract_id, iface, .. + } = contract.clone(); + + let desc = SecretString(alice_keys.public.rgb_udas_descriptor_xpub.clone()); + let req = RgbOfferRequest { + contract_id, + iface, + contract_amount: contract_amount.clone(), + bitcoin_price: 1_000, + descriptor: desc, + change_terminal: "/21/1".to_string(), + bitcoin_changes: vec![], + strategy: RgbSwapStrategy::Auction, + expire_at: None, + }; + + offers_collection.push(req); + } + + let offer_auction_req = RgbAuctionOfferRequest { + offers: offers_collection.clone(), + sign_keys: vec![ + SecretString(alice_keys.private.btc_descriptor_xprv.clone()), + SecretString(alice_keys.private.btc_change_descriptor_xprv.clone()), + SecretString(alice_keys.private.rgb_udas_descriptor_xprv.clone()), + ], + }; + + let resp = create_auction_offers(&alice_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); + + let RgbOfferResponse { + offer_id: offer_2nd, + contract_id: contract_2nd, + .. + } = offers.remove(0); + + // 6. Create Bid (1st Offer) + let bob_btc_desc = bob_keys.public.btc_descriptor_xpub.clone(); + let bid_auction_req = RgbAuctionBidRequest { + offer_id: offer_1st.clone(), + asset_amount: contract_amount.clone(), + descriptor: SecretString(bob_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(1000), + sign_keys: vec![ + SecretString(bob_keys.private.btc_descriptor_xprv.clone()), + SecretString(bob_keys.private.btc_change_descriptor_xprv.clone()), + ], + }; + + let resp = create_auction_bid(&bob_sk, bid_auction_req).await; + assert!(resp.is_ok()); + + // 7. Create Bid (2nd Offer) + let bob_btc_desc = bob_keys.public.btc_descriptor_xpub.clone(); + let bid_auction_req = RgbAuctionBidRequest { + offer_id: offer_2nd.clone(), + asset_amount: contract_amount.clone(), + descriptor: SecretString(bob_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(1000), + sign_keys: vec![ + SecretString(bob_keys.private.btc_descriptor_xprv.clone()), + SecretString(bob_keys.private.btc_change_descriptor_xprv.clone()), + ], + }; + + let resp = create_auction_bid(&bob_sk, bid_auction_req).await; + assert!(resp.is_ok()); + + // 7. Finish Offer + let resp = finish_auction_offers(&alice_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 = [bob_sk.clone(), alice_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(&bob_sk, &contract_1st).await; + assert!(resp.is_ok()); + assert_eq!(1., resp?.balance_normalized); + + let resp = get_contract(&alice_sk, &contract_1st).await; + assert!(resp.is_ok()); + assert_eq!(0., resp?.balance_normalized); + + // // 12. Check Balances (2nd Offer) + let resp = get_contract(&bob_sk, &contract_2nd).await; + assert!(resp.is_ok()); + assert_eq!(1., resp?.balance_normalized); + + let resp = get_contract(&alice_sk, &contract_2nd).await; + assert!(resp.is_ok()); + assert_eq!(0., resp?.balance_normalized); + + Ok(()) +} diff --git a/tests/rgb/web/swaps.rs b/tests/rgb/web/swaps.rs index e071da86..5954708f 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, FullIssueRequest, FullRgbTransferRequest, FundVaultDetails, @@ -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("");