diff --git a/.env b/.env index 1597b65c..2a16a2cc 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ +# :: Explorers :: # BITCOIN_EXPLORER_API_MAINNET=http://18.217.213.66:3000 # BITCOIN_EXPLORER_API_TESTNET=http://18.217.213.66:3000 # BITCOIN_EXPLORER_API_SIGNET=http://18.217.213.66:3000 @@ -15,12 +16,23 @@ BITCOIN_EXPLORER_API_REGTEST=http://localhost:3000/regtest/api # BITCOIN_ELECTRUM_API_MAINNET=mempool.space:50001 # BITCOIN_ELECTRUM_API_TESTNET=mempool.space:60001 # BITCOIN_ELECTRUM_API_SIGNET=mempool.space:60601 + +# :: LN :: LNDHUB_ENDPOINT=https://lndhubx-prod.bitmask.app #LNDHUB_ENDPOINT=https://lndhubx.bitmask.app -# CARBONADO_ENDPOINT=https://qvijq4x0ei.execute-api.us-east-2.amazonaws.com/dev/carbonado + +# :: Bitmask & Carbonado Server :: +STRESS_TEST=false +BITCOIN_NETWORK=regtest BITMASK_ENDPOINT=http://localhost:7070 CARBONADO_ENDPOINT=http://localhost:7070/carbonado -BITCOIN_NETWORK=regtest -STRESS_TEST=false +# CARBONADO_ENDPOINT=https://qvijq4x0ei.execute-api.us-east-2.amazonaws.com/dev/carbonado + +# :: Marketplace :: +UDAS_UTXO=3b367e1facc3174e97658295961faf6a4ed889129c881b7a73db1f074b49bd8a: MARKETPLACE_SEED=lion bronze dumb tuna perfect fantasy wall orphan improve business harbor sadness -UDAS_UTXO=3b367e1facc3174e97658295961faf6a4ed889129c881b7a73db1f074b49bd8a:0 +MARKETPLACE_NOSTR=cd591c134a0d88991326b1619953d0eae2287d315a7c4a93c1e4883a8c26c464 +# 1..100 +MARKETPLACE_FEE_PERC= +# xpub.. +MARKETPLACE_FEE_XPUB= diff --git a/Cargo.lock b/Cargo.lock index 576e656b..215e3b43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "baid58" -version = "0.4.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052064cc0caa02b62c88f06a7237304fb297873c78b6e95addecc3c5ddfce4ae" +checksum = "bc0585242d87ed976e05db6ae86a0f771f140104a4b6c91b4c3e43b9b2357486" dependencies = [ "base58", "blake3", @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "bdk" -version = "0.28.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b650f45ae7dc8558544448253f3e1ae443433637ccd9f9d14d2089ff913480" +checksum = "b15adb2017ab6437b6704a779ab8bbefe857612f5af9d84b677a1767f965e099" dependencies = [ "async-trait", "bdk-macros", @@ -644,6 +644,7 @@ dependencies = [ "autosurgeon", "axum", "axum-macros", + "baid58", "base64-compat", "base85", "bdk", @@ -658,6 +659,7 @@ dependencies = [ "bp-core", "bp-seals", "carbonado", + "chrono", "commit_verify", "console_error_panic_hook", "deflate", @@ -668,7 +670,7 @@ dependencies = [ "getrandom", "gloo-console", "gloo-net", - "gloo-utils 0.2.0", + "gloo-utils", "hex", "indexmap 1.9.3", "inflate", @@ -679,7 +681,7 @@ dependencies = [ "nostr-sdk", "once_cell", "payjoin", - "postcard 1.0.4", + "postcard 1.0.7", "pretty_env_logger", "psbt", "rand", @@ -917,18 +919,17 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets", ] [[package]] @@ -1505,7 +1506,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1527,11 +1528,11 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gloo-console" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" dependencies = [ - "gloo-utils 0.1.7", + "gloo-utils", "js-sys", "serde", "wasm-bindgen", @@ -1540,14 +1541,14 @@ dependencies = [ [[package]] name = "gloo-net" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils 0.1.7", + "gloo-utils", "http", "js-sys", "pin-project", @@ -1571,19 +1572,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gloo-utils" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "gloo-utils" version = "0.2.0" @@ -2107,9 +2095,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matchit" @@ -2161,7 +2149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys", ] @@ -2491,9 +2479,9 @@ dependencies = [ [[package]] name = "postcard" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00" +checksum = "d534c6e61df1c7166e636ca612d9820d486fe96ddad37f7abc671517b297488e" dependencies = [ "cobs", "heapless", @@ -2633,9 +2621,9 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.2", "bytes", @@ -2670,7 +2658,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.22.6", + "webpki-roots 0.25.2", "winreg", ] @@ -2850,13 +2838,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", - "rustls-webpki 0.101.1", + "rustls-webpki 0.101.5", "sct", ] @@ -2881,9 +2869,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.1" +version = "0.101.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" dependencies = [ "ring", "untrusted", @@ -3178,7 +3166,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.23", + "time", ] [[package]] @@ -3496,17 +3484,6 @@ dependencies = [ "syn 2.0.26", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.23" @@ -3918,12 +3895,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4044,21 +4015,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "webpki", + "rustls-webpki 0.100.1", ] [[package]] name = "webpki-roots" -version = "0.23.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" -dependencies = [ - "rustls-webpki 0.100.1", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "winapi" @@ -4177,11 +4145,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 96a8e267..84897786 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,14 +56,14 @@ getrandom = { version = "0.2.10", features = ["js"] } hex = "0.4.3" indexmap = "1.9.3" lightning-invoice = "0.23.0" -log = "0.4.17" +log = "0.4.20" miniscript_crate = { package = "miniscript", version = "9.0.1", features = [ "compiler", ] } nostr-sdk = "0.22.0" once_cell = "1.17.1" payjoin = { version = "0.8.0", features = ["send"] } -postcard = { version = "1.0.4", features = ["alloc"] } +postcard = { version = "1.0.7", features = ["alloc"] } pretty_env_logger = "0.5.0" psbt = { version = "0.10.0-alpha.2", features = [ "sign", @@ -72,7 +72,7 @@ psbt = { version = "0.10.0-alpha.2", features = [ "construct", ] } regex = "1.7.0" -reqwest = { version = "0.11.18", features = ["json"] } +reqwest = { version = "0.11.20", features = ["json"] } rgb-std = { version = "0.10.4" } rgb-wallet = { version = "0.10.4" } rgb-schemata = { version = "0.10.0-rc.2" } @@ -89,14 +89,16 @@ blake3 = "1.4.1" base85 = "2.0.0" automerge = "0.5.1" autosurgeon = "0.8" +baid58 = "0.4.4" +chrono = "0.4" [target.'cfg(target_arch = "wasm32")'.dependencies] -bdk = { version = "0.28.0", features = [ +bdk = { version = "0.28.2", features = [ "use-esplora-async", "async-interface", ], default-features = false } -gloo-console = "0.2.3" -gloo-net = { version = "0.3.1", features = ["http"] } +gloo-console = "0.3.0" +gloo-net = { version = "0.4.0", features = ["http"] } gloo-utils = "0.2.0" js-sys = "0.3.63" serde-wasm-bindgen = "0.5.0" @@ -104,6 +106,7 @@ wasm-bindgen = { version = "0.2.86", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.36" web-sys = "0.3.63" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] bdk = { version = "0.28.0", features = [ "use-esplora-async", @@ -125,6 +128,7 @@ tokio = { version = "1.28.2", features = ["full"] } [dev-dependencies] wasm-bindgen-test = "0.3.36" + [build-dependencies] anyhow = "1.0.71" blake3 = "1.4.1" diff --git a/src/bin/bitmaskd.rs b/src/bin/bitmaskd.rs index 4bea98fe..e9cbc181 100644 --- a/src/bin/bitmaskd.rs +++ b/src/bin/bitmaskd.rs @@ -15,9 +15,11 @@ use axum::{ }; use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, SecretKey}; use bitmask_core::{ - bitcoin::{save_mnemonic, sign_psbt_file}, - carbonado::handle_file, - constants::{get_marketplace_seed, get_network, get_udas_utxo, switch_network}, + bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, + carbonado::{handle_file, server_retrieve, server_store, store}, + constants::{ + get_marketplace_nostr_key, get_marketplace_seed, get_network, get_udas_utxo, switch_network, + }, rgb::{ accept_transfer, clear_watcher as rgb_clear_watcher, create_invoice, create_psbt, create_watcher, full_transfer_asset, import as rgb_import, issue_contract, list_contracts, @@ -141,7 +143,7 @@ async fn _sign_psbt( Json(psbt_req): Json, ) -> Result { info!("POST /sign {psbt_req:?}"); - let psbt_res = sign_psbt_file(psbt_req).await?; + let psbt_res = sign_and_publish_psbt_file(psbt_req).await?; Ok((StatusCode::OK, Json(psbt_res))) } @@ -493,6 +495,46 @@ async fn co_force_store( Ok((StatusCode::OK, TypedHeader(cc))) } +async fn co_server_store( + Path(name): Path, + body: Bytes, +) -> Result { + info!("POST /carbonado/server/{name}, {} bytes", body.len()); + let (filepath, encoded) = server_store(&name, &body, None).await?; + + match OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&filepath) + { + Ok(file) => { + let present_header = match carbonado::file::Header::try_from(&file) { + Ok(header) => header, + _ => carbonado::file::Header::try_from(&body)?, + }; + let present_len = present_header.encoded_len - present_header.padding_len; + debug!("present_len: {present_len}"); + let resp = fs::write(&filepath, &encoded).await; + debug!("file override status {}", resp.is_ok()); + } + Err(err) => match err.kind() { + ErrorKind::NotFound => { + debug!("no file found, writing 0 bytes."); + fs::write(&filepath, &body).await?; + } + _ => { + error!("error in POST /carbonado/server/{name}: {err}"); + return Err(err.into()); + } + }, + } + + let cc = CacheControl::new().with_no_cache(); + + Ok((StatusCode::OK, TypedHeader(cc))) +} + async fn co_retrieve( Path((pk, name)): Path<(String, String)>, ) -> Result { @@ -555,6 +597,24 @@ async fn co_metadata( Ok((StatusCode::OK, Json(metadata))) } +async fn co_server_retrieve(Path(name): Path) -> Result { + info!("GET /server/{name}"); + + let result = server_retrieve(&name).await; + let cc = CacheControl::new().with_no_cache(); + + match result { + Ok((bytes, _)) => { + debug!("read {0} bytes.", bytes.len()); + Ok((StatusCode::OK, TypedHeader(cc), bytes)) + } + Err(e) => { + debug!("file read error {0} .Details: {1}.", name, e.to_string()); + Ok((StatusCode::OK, TypedHeader(cc), Vec::::new())) + } + } +} + const BMC_VERSION: &str = env!("CARGO_PKG_VERSION"); async fn status() -> Result { @@ -631,6 +691,8 @@ async fn main() -> Result<()> { .route("/transfers/", delete(remove_transfer)) .route("/key/:pk", get(key)) .route("/carbonado/status", get(status)) + .route("/carbonado/server/:name", get(co_server_retrieve)) + .route("/carbonado/server/:name", post(co_server_store)) .route("/carbonado/:pk/:name", get(co_retrieve)) .route("/carbonado/:pk/:name", post(co_store)) .route("/carbonado/:pk/:name/force", post(co_force_store)) diff --git a/src/bitcoin.rs b/src/bitcoin.rs index ea6dca38..dc5ff573 100644 --- a/src/bitcoin.rs +++ b/src/bitcoin.rs @@ -2,9 +2,10 @@ use std::str::FromStr; use ::bitcoin::util::address::Address; use ::psbt::Psbt; +use amplify::hex::ToHex; use argon2::Argon2; use bdk::{wallet::AddressIndex, FeeRate, LocalUtxo, SignOptions, TransactionDetails}; -use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::{consensus::encode, psbt::PartiallySignedTransaction}; use rand::{rngs::StdRng, Rng, SeedableRng}; use serde_encrypt::{ serialize::impls::BincodeSerializer, shared_key::SharedKey, traits::SerdeEncryptSharedKey, @@ -23,18 +24,23 @@ pub use crate::bitcoin::{ assets::dust_tx, keys::{new_mnemonic, save_mnemonic, BitcoinKeysError}, payment::{create_payjoin, create_transaction, BitcoinPaymentError}, - psbt::{sign_psbt, sign_psbt_with_multiple_wallets, BitcoinPsbtError}, + psbt::{ + multi_sign_and_publish_psbt, multi_sign_psbt, sign_and_publish_psbt, sign_psbt, + BitcoinPsbtError, + }, wallet::{ get_blockchain, get_wallet, sync_wallet, sync_wallets, BitcoinWalletError, MemoryWallet, }, }; use crate::{ + bitcoin::keys::get_marketplace_descriptor, constants::{DIBA_DESCRIPTOR, DIBA_DESCRIPTOR_VERSION, DIBA_MAGIC_NO, NETWORK}, debug, info, structs::{ - DecryptedWalletData, EncryptedWalletDataV04, FundVaultDetails, SatsInvoice, SecretString, - SignPsbtRequest, SignPsbtResponse, WalletData, WalletTransaction, + DecryptedWalletData, EncryptedWalletDataV04, FundVaultDetails, PublishedPsbtResponse, + SatsInvoice, SecretString, SignPsbtRequest, SignedPsbtResponse, WalletData, + WalletTransaction, }, trace, }; @@ -297,6 +303,18 @@ pub async fn get_wallet_data( }) } +pub async fn get_swap_new_address() -> Result, BitcoinError> { + info!("get_swap_new_address"); + + let markplace_desc = get_marketplace_descriptor().await?; + if let Some(markplace_desc) = markplace_desc { + let address = get_new_address(&markplace_desc, None).await?; + return Ok(Some(address)); + } + + Ok(None) +} + pub async fn get_new_address( descriptor: &SecretString, change_descriptor: Option<&SecretString>, @@ -472,7 +490,31 @@ pub async fn get_assets_vault( }) } -pub async fn sign_psbt_file(request: SignPsbtRequest) -> Result { +pub async fn sign_psbt_file(request: SignPsbtRequest) -> Result { + let SignPsbtRequest { psbt, descriptors } = request; + + let original_psbt = Psbt::from_str(&psbt)?; + let final_psbt = PartiallySignedTransaction::from(original_psbt); + + let mut wallets = vec![]; + for descriptor in descriptors { + let wallet = get_wallet(&descriptor, None).await?; + wallets.push(wallet); + } + + let psbt_signed = multi_sign_psbt(wallets, final_psbt).await?; + + let psbt_bytes = encode::serialize(&psbt_signed); + let psbt_hex = psbt_bytes.to_hex(); + Ok(SignedPsbtResponse { + sign: true, + psbt: psbt_hex, + }) +} + +pub async fn sign_and_publish_psbt_file( + request: SignPsbtRequest, +) -> Result { let SignPsbtRequest { psbt, descriptors } = request; let original_psbt = Psbt::from_str(&psbt)?; @@ -484,13 +526,13 @@ pub async fn sign_psbt_file(request: SignPsbtRequest) -> Result SignPsbtResponse { + Some(tx) => PublishedPsbtResponse { sign: true, txid: tx.txid().to_string(), }, - _ => SignPsbtResponse { + _ => PublishedPsbtResponse { sign: false, txid: String::new(), }, diff --git a/src/bitcoin/keys.rs b/src/bitcoin/keys.rs index d8c53497..3eafcb61 100644 --- a/src/bitcoin/keys.rs +++ b/src/bitcoin/keys.rs @@ -9,15 +9,18 @@ use bdk::{ miniscript::{descriptor::DescriptorKeyParseError, Tap}, }; use bip39::{Language, Mnemonic}; -use bitcoin::KeyPair; +use bitcoin::{KeyPair, Network}; use bitcoin_hashes::{sha256, Hash}; -use miniscript_crate::DescriptorPublicKey; +use miniscript_crate::{ + descriptor::{DescriptorXKey, Wildcard}, + DescriptorPublicKey, +}; use nostr_sdk::prelude::{FromSkStr, ToBech32}; use thiserror::Error; use zeroize::Zeroize; use crate::{ - constants::{BTC_PATH, NETWORK}, + constants::{get_marketplace_fee_xpub, get_network, BTC_PATH, NETWORK}, structs::{DecryptedWalletData, PrivateWalletData, PublicWalletData, SecretString}, }; @@ -204,3 +207,35 @@ pub async fn get_mnemonic( public, }) } + +pub async fn get_marketplace_descriptor() -> Result, BitcoinKeysError> { + let btc_path = BTC_PATH.read().await; + let marketplace_xpub = get_marketplace_fee_xpub().await; + let network = get_network().await; + let network = Network::from_str(&network).expect("wrong network"); + + if marketplace_xpub.is_empty() { + return Ok(None); + } + + let path = DerivationPath::from_str(&btc_path)?; + let deriv = DerivationPath::default().child(ChildNumber::from_normal_idx(0)?); + let mut xkey = + ExtendedPubKey::from_str(&marketplace_xpub).expect("wrong marketplace xpub format"); + xkey.network = network; + + let fp = xkey.fingerprint(); + let origin: KeySource = (fp, path.clone()); + + let desc = DescriptorXKey:: { + origin: Some(origin), + xkey, + derivation_path: deriv, + wildcard: Wildcard::None, + }; + + let desc_xpub = DescriptorPublicKey::XPub(desc).to_string(); + let tap = format!("tr({desc_xpub}/*)"); + + Ok(Some(SecretString(tap))) +} diff --git a/src/bitcoin/payment.rs b/src/bitcoin/payment.rs index eb5edb86..1608cc01 100644 --- a/src/bitcoin/payment.rs +++ b/src/bitcoin/payment.rs @@ -10,7 +10,7 @@ use thiserror::Error; use crate::{ bitcoin::{ - psbt::{sign_original_psbt, sign_psbt, BitcoinPsbtError}, + psbt::{sign_and_publish_psbt, sign_psbt, BitcoinPsbtError}, wallet::MemoryWallet, }, debug, info, @@ -58,7 +58,7 @@ pub async fn create_transaction( debug!(format!("Create transaction: {details:#?}")); debug!("Unsigned PSBT:", base64::encode(&serialize(&psbt))); - let details = sign_psbt(wallet, psbt).await?; + let details = sign_and_publish_psbt(wallet, psbt).await?; info!("PSBT successfully signed"); Ok(details) @@ -83,7 +83,7 @@ pub async fn create_payjoin( debug!(format!("Request PayJoin transaction: {details:#?}")); debug!("Unsigned Original PSBT:", base64::encode(&serialize(&psbt))); - let original_psbt = sign_original_psbt(wallet, psbt.clone()).await?; + let original_psbt = sign_psbt(wallet, psbt.clone()).await?; info!("Original PSBT successfully signed"); let additional_fee_index = psbt @@ -146,7 +146,7 @@ pub async fn create_payjoin( base64::encode(&serialize(&payjoin_psbt)) ); // sign_psbt also broadcasts; - let tx = sign_psbt(wallet, payjoin_psbt).await?; + let tx = sign_and_publish_psbt(wallet, payjoin_psbt).await?; Ok(tx) } diff --git a/src/bitcoin/psbt.rs b/src/bitcoin/psbt.rs index 9eeb8c18..bdb91406 100644 --- a/src/bitcoin/psbt.rs +++ b/src/bitcoin/psbt.rs @@ -20,10 +20,52 @@ pub enum BitcoinPsbtError { BdkEsploraError(#[from] bdk::esplora_client::Error), } -/// Signs and broadcasts a transaction given a Psbt +// Only signs an original psbt. pub async fn sign_psbt( wallet: &MemoryWallet, mut psbt: PartiallySignedTransaction, +) -> Result { + debug!("Funding PSBT..."); + let opts = SignOptions { + allow_all_sighashes: true, + remove_partial_sigs: false, + ..Default::default() + }; + wallet.lock().await.sign(&mut psbt, opts)?; + Ok(psbt) +} + +pub async fn multi_sign_psbt( + wallets: Vec, + mut psbt: PartiallySignedTransaction, +) -> Result { + let total_wallets = wallets.len(); + debug!(format!( + "Signing PSBT ({total_wallets}/{total_wallets}) ..." + )); + + let mut sign_count = 0; + for wallet in wallets { + wallet.lock().await.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + remove_partial_sigs: false, + ..Default::default() + }, + )?; + + sign_count += 1; + debug!(format!("PSBT Sign: ({sign_count}/{total_wallets})")); + } + + Ok(psbt) +} + +/// Signs and broadcasts a transaction given a Psbt +pub async fn sign_and_publish_psbt( + wallet: &MemoryWallet, + mut psbt: PartiallySignedTransaction, ) -> Result { debug!("Signing PSBT..."); let finalized = wallet @@ -65,21 +107,8 @@ pub async fn sign_psbt( } } -// Only signs an original psbt. -pub async fn sign_original_psbt( - wallet: &MemoryWallet, - mut psbt: PartiallySignedTransaction, -) -> Result { - debug!("Funding PSBT..."); - let opts = SignOptions { - remove_partial_sigs: false, - ..Default::default() - }; - wallet.lock().await.sign(&mut psbt, opts)?; - Ok(psbt) -} - -pub async fn sign_psbt_with_multiple_wallets( +/// Signs and broadcasts a transaction given a Psbt +pub async fn multi_sign_and_publish_psbt( wallets: Vec, mut psbt: PartiallySignedTransaction, ) -> Result { @@ -91,10 +120,14 @@ pub async fn sign_psbt_with_multiple_wallets( let mut sign_count = 0; let mut finalized = false; for wallet in wallets { - finalized = wallet - .lock() - .await - .sign(&mut psbt, SignOptions::default())?; + finalized = wallet.lock().await.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + remove_partial_sigs: false, + ..Default::default() + }, + )?; sign_count += 1; debug!(format!("PSBT Sign: ({sign_count}/{total_wallets})")); diff --git a/src/carbonado.rs b/src/carbonado.rs index 72698310..4ae54bef 100644 --- a/src/carbonado.rs +++ b/src/carbonado.rs @@ -6,10 +6,12 @@ use crate::{carbonado::error::CarbonadoError, constants::NETWORK, info, structs: pub mod error; #[cfg(not(target_arch = "wasm32"))] -pub use server::{handle_file, retrieve, retrieve_metadata, store}; +pub use server::{handle_file, retrieve, retrieve_metadata, server_retrieve, server_store, store}; #[cfg(not(target_arch = "wasm32"))] mod server { + use crate::constants::get_marketplace_nostr_key; + use super::*; use std::{ @@ -40,6 +42,27 @@ mod server { Ok(()) } + pub async fn server_store( + name: &str, + input: &[u8], + metadata: Option>, + ) -> Result<(PathBuf, Vec), CarbonadoError> { + let marketplace_key: String = get_marketplace_nostr_key().await; + + let level = 15; + let sk = hex::decode(marketplace_key)?; + 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 = hex::encode(pk); + + let meta: Option<[u8; 8]> = metadata.map(|m| m.try_into().expect("invalid metadata size")); + let (body, _encode_info) = carbonado::file::encode(&sk, Some(&pk), input, level, meta)?; + let filepath = handle_file(&pk_hex, name, body.len()).await?; + fs::write(filepath.clone(), body.clone()).await?; + Ok((filepath, body)) + } + pub async fn retrieve( sk: &str, name: &str, @@ -82,6 +105,30 @@ mod server { Ok((Vec::new(), None)) } + pub async fn server_retrieve(name: &str) -> Result<(Vec, Option>), CarbonadoError> { + let marketplace_key: String = get_marketplace_nostr_key().await; + + let sk = hex::decode(marketplace_key)?; + 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, @@ -148,7 +195,7 @@ mod server { } #[cfg(target_arch = "wasm32")] -pub use client::{retrieve, retrieve_metadata, store}; +pub use client::{retrieve, retrieve_metadata, server_retrieve, server_store, store}; #[cfg(target_arch = "wasm32")] mod client { @@ -228,6 +275,38 @@ mod client { } } + pub async fn server_store( + name: &str, + input: &[u8], + _metadata: Option>, + ) -> Result<(), CarbonadoError> { + let body = Arc::new(input.to_vec()); + let network = NETWORK.read().await.to_string(); + let endpoints = CARBONADO_ENDPOINT.read().await.to_string(); + let endpoints: Vec<&str> = endpoints.split(',').collect(); + let requests = Array::new(); + + for endpoint in endpoints { + let url = format!("{endpoint}/server/{network}-{name}"); + let fetch_fn = future_to_promise(fetch_post(url, body.clone())); + requests.push(&fetch_fn); + } + + let results = JsFuture::from(Promise::all_settled(&JsValue::from(requests))) + .await + .map_err(js_to_error)?; + + info!(format!("Store results: {results:?}")); + + let results = serde_wasm_bindgen::from_value::>(results)?; + let success = results.iter().any(|result| result.value == 200.0); + if success { + Ok(()) + } else { + Err(CarbonadoError::AllEndpointsFailed) + } + } + pub async fn retrieve_metadata(sk: &str, name: &str) -> Result { let sk = hex::decode(sk)?; let secret_key = SecretKey::from_slice(&sk)?; @@ -316,6 +395,28 @@ mod client { Ok((Vec::new(), None)) } + pub async fn server_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(); + + let requests = Array::new(); + for endpoint in endpoints.iter() { + let url = format!("{endpoint}/server/{network}-{name}"); + let fetch_fn = future_to_promise(fetch_get_byte_array(url)); + requests.push(&fetch_fn); + } + + let result = JsFuture::from(Promise::any(&JsValue::from(requests))) + .await + .map_err(js_to_error)?; + + let array = Uint8Array::from(result); + let encoded = array.to_vec(); + + Ok((encoded.to_vec(), None)) + } + async fn fetch_post(url: String, body: Arc>) -> Result { 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 59409d9f..aa3bb9a6 100644 --- a/src/carbonado/error.rs +++ b/src/carbonado/error.rs @@ -25,6 +25,8 @@ pub enum CarbonadoError { SerdeWasm(#[from] serde_wasm_bindgen::Error), /// All endpoints failed error AllEndpointsFailed, + /// Wrong Nostr private key + WrongNostrPrivateKey, /// Debug: {0} Debug(String), } diff --git a/src/constants.rs b/src/constants.rs index 3bd3204c..7ad2dcfe 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -51,10 +51,31 @@ pub static BITCOIN_ELECTRUM_API: Lazy> = pub static MARKETPLACE_SEED: Lazy> = Lazy::new(|| RwLock::new(dot_env("MARKETPLACE_SEED"))); +pub static MARKETPLACE_NOSTR: Lazy> = + Lazy::new(|| RwLock::new(dot_env("MARKETPLACE_NOSTR"))); + +pub static MARKETPLACE_FEE_PERC: Lazy> = + Lazy::new(|| RwLock::new(dot_env("MARKETPLACE_FEE_PERC"))); + +pub static MARKETPLACE_FEE_XPUB: Lazy> = + Lazy::new(|| RwLock::new(dot_env("MARKETPLACE_FEE_XPUB"))); + pub async fn get_marketplace_seed() -> String { MARKETPLACE_SEED.read().await.to_string() } +pub async fn get_marketplace_nostr_key() -> String { + MARKETPLACE_NOSTR.read().await.to_string() +} + +pub async fn get_marketplace_fee_percentage() -> String { + MARKETPLACE_FEE_PERC.read().await.to_string() +} + +pub async fn get_marketplace_fee_xpub() -> String { + MARKETPLACE_FEE_XPUB.read().await.to_string() +} + pub static UDAS_UTXO: Lazy> = Lazy::new(|| RwLock::new(dot_env("UDAS_UTXO"))); pub async fn get_udas_utxo() -> String { @@ -192,4 +213,8 @@ pub mod storage_keys { pub const ASSETS_STOCK: &str = "bitmask-fungible_assets_stock.c15"; pub const ASSETS_WALLETS: &str = "bitmask-fungible_assets_wallets.c15"; pub const ASSETS_TRANSFERS: &str = "bitmask_assets_transfers.c15"; + pub const ASSETS_OFFERS: &str = "bitmask-asset_offers.c15"; + pub const ASSETS_BIDS: &str = "bitmask-asset_bids.c15"; + pub const MARKETPLACE_OFFERS: &str = "bitmask-marketplace_public_offers.c15"; + pub const MARKETPLACE_BIDS: &str = "bitmask-marketplace_public_bids.c15"; } diff --git a/src/rgb.rs b/src/rgb.rs index 6c5365ae..ed47f9f2 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -1,14 +1,15 @@ -use ::psbt::serialize::Serialize; +use ::psbt::{serialize::Serialize, Psbt}; use amplify::{ confinement::U32, hex::{FromHex, ToHex}, }; use anyhow::Result; use autosurgeon::reconcile; -use bitcoin::{Network, Txid}; +use bitcoin::{psbt::PartiallySignedTransaction, EcdsaSighashType, Network, Txid}; use bitcoin_30::bip32::ExtendedPubKey; use bitcoin_scripts::address::AddressNetwork; use garde::Validate; + use miniscript_crate::DescriptorPublicKey; use rgb::RgbDescr; use rgbstd::{ @@ -20,7 +21,8 @@ use rgbstd::{ }; use rgbwallet::{psbt::DbcPsbtError, RgbInvoice}; use std::{ - collections::{BTreeMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, + ops::Sub, str::FromStr, }; use strict_encoding::{tn, StrictSerialize}; @@ -39,6 +41,7 @@ pub mod prefetch; pub mod psbt; pub mod resolvers; pub mod structs; +pub mod swap; pub mod transfer; pub mod wallet; @@ -60,11 +63,14 @@ use crate::{ ImportRequest, InterfaceDetail, InterfacesResponse, InvoiceRequest, InvoiceResponse, IssueMetaRequest, IssueMetadata, IssueRequest, IssueResponse, NewCollectible, NextAddressResponse, NextUtxoResponse, NextUtxosResponse, PsbtFeeRequest, PsbtRequest, - PsbtResponse, ReIssueRequest, ReIssueResponse, RgbInvoiceResponse, - RgbRemoveTransferRequest, RgbSaveTransferRequest, RgbTransferDetail, RgbTransferRequest, - RgbTransferResponse, RgbTransferStatusResponse, RgbTransfersResponse, SchemaDetail, - SchemasResponse, TransferType, TxStatus, UDADetail, UtxoResponse, WatcherDetailResponse, - WatcherRequest, WatcherResponse, WatcherUtxoResponse, + PsbtResponse, PublicRgbBidResponse, PublicRgbOfferResponse, PublicRgbOffersResponse, + ReIssueRequest, ReIssueResponse, RgbBidDetail, RgbBidRequest, RgbBidResponse, + RgbBidsResponse, RgbInvoiceResponse, RgbOfferBidsResponse, RgbOfferDetail, RgbOfferRequest, + RgbOfferResponse, RgbOffersResponse, RgbRemoveTransferRequest, RgbSaveTransferRequest, + RgbSwapRequest, RgbSwapResponse, RgbTransferDetail, RgbTransferInternalParams, + RgbTransferRequest, RgbTransferResponse, RgbTransferStatusResponse, RgbTransfersResponse, + SchemaDetail, SchemasResponse, TransferType, TxStatus, UDADetail, UtxoResponse, + WatcherDetailResponse, WatcherRequest, WatcherResponse, WatcherUtxoResponse, }, validators::RGBContext, }; @@ -74,23 +80,33 @@ use self::{ contract::{export_contract, ExportContractError}, crdt::{LocalRgbAccount, RawRgbAccount, RgbMerge}, fs::{ - retrieve_account, retrieve_local_account, retrieve_stock as retrieve_rgb_stock, - retrieve_stock_account, retrieve_stock_account_transfers, retrieve_stock_transfers, - retrieve_transfers, store_account, store_local_account, store_stock as store_rgb_stock, - store_stock_account, store_stock_account_transfers, store_stock_transfers, store_transfers, - RgbPersistenceError, + retrieve_account, retrieve_bids, retrieve_local_account, retrieve_offers, + retrieve_public_offers, retrieve_stock as retrieve_rgb_stock, retrieve_stock_account, + retrieve_stock_account_transfers, retrieve_stock_transfers, retrieve_transfers, + store_account, store_bids, store_local_account, store_offers, + store_stock as store_rgb_stock, store_stock_account, store_stock_account_transfers, + store_stock_transfers, store_transfers, RgbPersistenceError, }, import::{import_contract, ImportContractError}, - prebuild::prebuild_transfer_asset, + prebuild::{ + prebuild_buyer_swap, prebuild_extract_transfer, prebuild_seller_swap, + prebuild_transfer_asset, + }, prefetch::{ prefetch_resolver_allocations, prefetch_resolver_images, prefetch_resolver_import_rgb, prefetch_resolver_psbt, prefetch_resolver_rgb, prefetch_resolver_txs_status, prefetch_resolver_user_utxo_status, prefetch_resolver_utxos, prefetch_resolver_waddress, prefetch_resolver_wutxo, }, - psbt::{save_commit, CreatePsbtError, EstimateFeeError}, - structs::{RgbAccount, RgbTransfer, RgbTransfers}, - transfer::{extract_transfer, AcceptTransferError, NewInvoiceError, NewPaymentError}, + psbt::{save_commit, set_tapret_position, CreatePsbtError, EstimateFeeError}, + structs::{RgbAccount, RgbExtractTransfer, RgbTransfer, RgbTransfers}, + swap::{ + get_public_offer, get_swap_bid, 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, TransferSwap, TransferSwapError, + }, + transfer::{AcceptTransferError, NewInvoiceError, NewPaymentError}, wallet::{ create_wallet, next_address, next_utxo, next_utxos, register_address, register_utxo, sync_wallet, @@ -413,6 +429,19 @@ pub async fn create_invoice( sk: &str, request: InvoiceRequest, ) -> Result { + let mut stock = retrieve_rgb_stock(sk).await.map_err(InvoiceError::IO)?; + let invoice = internal_create_invoice(request, &mut stock).await?; + store_rgb_stock(sk, stock).await.map_err(InvoiceError::IO)?; + + Ok(InvoiceResponse { + invoice: invoice.to_string(), + }) +} + +async fn internal_create_invoice( + request: InvoiceRequest, + stock: &mut Stock, +) -> Result { if let Err(err) = request.validate(&RGBContext::default()) { let errors = err .flatten() @@ -432,28 +461,15 @@ pub async fn create_invoice( let network = NETWORK.read().await.to_string(); - let mut stock = retrieve_rgb_stock(sk).await.map_err(InvoiceError::IO)?; - let invoice = create_rgb_invoice( - &contract_id, - &iface, - amount, - &seal, - &network, - params, - &mut stock, - ) - .map_err(InvoiceError::Invoice)?; - - store_rgb_stock(sk, stock).await.map_err(InvoiceError::IO)?; + let invoice = create_rgb_invoice(&contract_id, &iface, amount, &seal, &network, params, stock) + .map_err(InvoiceError::Invoice)?; - Ok(InvoiceResponse { - invoice: invoice.to_string(), - }) + Ok(invoice) } #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] -pub enum TransferError { +pub enum PsbtError { /// Some request data is missing. {0:?} Validation(BTreeMap), /// Retrieve I/O or connectivity error. {0:?} @@ -462,8 +478,6 @@ pub enum TransferError { NoWatcher, /// Contract is required in this operation. Please, import or issue a Contract. NoContract, - /// Iface is required in this operation. Please, use the correct iface contract. - NoIface, /// FeeRate is supported in this operation. Please, use the absolute fee value. NoFeeRate, /// Insufficient funds (expected: {input} sats / current: {output} sats) @@ -478,51 +492,41 @@ pub enum TransferError { WrongAutoMerge(String), /// Occurs an error in create step. {0} Create(CreatePsbtError), - /// Occurs an error in estimate fee step. {0} - Estimate(EstimateFeeError), - /// Occurs an error in commitment step. {0} - Commitment(DbcPsbtError), - /// Occurs an error in payment step. {0} - Pay(NewPaymentError), - /// Occurs an error in accept step. {0} - Accept(AcceptTransferError), - /// Consignment cannot be encoded. - WrongConsig(String), - /// Rgb Invoice cannot be decoded. {0} - WrongInvoice(String), /// Bitcoin network be decoded. {0} WrongNetwork(String), /// Occurs an error in export step. {0} Export(ExportContractError), } -pub async fn create_psbt(sk: &str, request: PsbtRequest) -> Result { +pub async fn create_psbt(sk: &str, request: PsbtRequest) -> Result { let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() }; - let mut rgb_account = retrieve_account(sk).await.map_err(TransferError::IO)?; - let psbt = internal_create_psbt(request, &mut rgb_account, &mut resolver).await?; + let mut rgb_account = retrieve_account(sk).await.map_err(PsbtError::IO)?; + let psbt = internal_create_psbt(request, true, None, &mut rgb_account, &mut resolver).await?; Ok(psbt) } async fn internal_create_psbt( request: PsbtRequest, + set_tapret: bool, + sighash: Option, rgb_account: &mut RgbAccount, resolver: &mut ExplorerResolver, -) -> Result { +) -> Result { if let Err(err) = request.validate(&RGBContext::default()) { let errors = err .flatten() .into_iter() .map(|(f, e)| (f, e.to_string())) .collect(); - return Err(TransferError::Validation(errors)); + return Err(PsbtError::Validation(errors)); } if rgb_account.wallets.get("default").is_none() { - return Err(TransferError::NoWatcher); + return Err(PsbtError::NoWatcher); } let PsbtRequest { @@ -543,19 +547,25 @@ async fn internal_create_psbt( // Retrieve transaction fee let fee = match fee { PsbtFeeRequest::Value(fee) => fee, - PsbtFeeRequest::FeeRate(_) => return Err(TransferError::NoFeeRate), + PsbtFeeRequest::FeeRate(_) => return Err(PsbtError::NoFeeRate), }; let wallet = rgb_account.wallets.get("default"); - let (psbt_file, change_terminal) = create_rgb_psbt( + let (mut psbt_file, change_terminal) = create_rgb_psbt( all_inputs, bitcoin_changes, fee, + sighash, asset_terminal_change, wallet.cloned(), resolver, ) - .map_err(TransferError::Create)?; + .map_err(PsbtError::Create)?; + + if set_tapret { + let pos = (psbt_file.outputs.len() - 1) as u16; + psbt_file = set_tapret_position(psbt_file, pos).map_err(PsbtError::Create)?; + } let psbt = PsbtResponse { psbt: Serialize::serialize(&psbt_file).to_hex(), @@ -565,6 +575,53 @@ async fn internal_create_psbt( Ok(psbt) } +#[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] +#[display(doc_comments)] +pub enum TransferError { + /// Some request data is missing. {0:?} + Validation(BTreeMap), + /// Retrieve I/O or connectivity error. {0:?} + IO(RgbPersistenceError), + /// Watcher is required in this operation. Please, create watcher. + NoWatcher, + /// Contract is required in this operation. Please, import or issue a Contract. + NoContract, + /// Iface is required in this operation. Please, use the correct iface contract. + NoIface, + /// FeeRate is supported in this operation. Please, use the absolute fee value. + NoFeeRate, + /// Insufficient funds (expected: {input} sats / current: {output} sats) + Inflation { + /// Amount spent: input amounts + input: u64, + + /// Amount sent: sum of output value + transaction fee + output: u64, + }, + /// Auto merge fail in this opration + WrongAutoMerge(String), + /// Occurs an error in create step. {0} + Create(PsbtError), + /// Occurs an error in estimate fee step. {0} + Estimate(EstimateFeeError), + /// Occurs an error in commitment step. {0} + Commitment(DbcPsbtError), + /// Occurs an error in payment step. {0} + Pay(NewPaymentError), + /// Occurs an error in accept step. {0} + Accept(AcceptTransferError), + /// Consignment cannot be encoded. + WrongConsig(String), + /// Rgb Invoice cannot be decoded. {0} + WrongInvoice(String), + /// Bitcoin network be decoded. {0} + WrongNetwork(String), + /// Occurs an error in swa´p step. {0} + WrongSwap(RgbOfferErrors), + /// Occurs an error in export step. {0} + Export(ExportContractError), +} + pub async fn full_transfer_asset( sk: &str, request: FullRgbTransferRequest, @@ -623,15 +680,20 @@ pub async fn full_transfer_asset( asset_terminal_change: Some(change_terminal), }; - let psbt_response = internal_create_psbt(psbt_req, &mut rgb_account, &mut resolver).await?; + let psbt_response = internal_create_psbt(psbt_req, true, None, &mut rgb_account, &mut resolver) + .await + .map_err(TransferError::Create)?; + let transfer_req = RgbTransferRequest { rgb_invoice, psbt: psbt_response.psbt, terminal: psbt_response.terminal, }; + let params = RgbTransferInternalParams::default(); let resp = internal_transfer_asset( transfer_req, + params, &mut stock, &mut rgb_account, &mut rgb_transfers, @@ -661,8 +723,15 @@ pub async fn transfer_asset( .await .map_err(TransferError::IO)?; - let resp = - internal_transfer_asset(request, &mut stock, &mut rgb_account, &mut rgb_transfers).await?; + let params = RgbTransferInternalParams::default(); + let resp = internal_transfer_asset( + request, + params, + &mut stock, + &mut rgb_account, + &mut rgb_transfers, + ) + .await?; store_stock_account_transfers(sk, stock, rgb_account, rgb_transfers) .await @@ -673,6 +742,7 @@ pub async fn transfer_asset( async fn internal_transfer_asset( request: RgbTransferRequest, + params: RgbTransferInternalParams, stock: &mut Stock, rgb_account: &mut RgbAccount, rgb_transfers: &mut RgbTransfers, @@ -710,9 +780,16 @@ async fn internal_transfer_asset( .insert(RGB_DEFAULT_NAME.to_string(), wallet.clone()); }; - let consig = transfer - .to_strict_serialized::<{ U32 }>() - .map_err(|err| TransferError::WrongConsig(err.to_string()))?; + let consig_id = transfer.bindle_id().to_string(); + let consig = if let (Some(offer_id), Some(bid_id)) = (params.offer_id, params.bid_id) { + let swap = TransferSwap::with(&offer_id, &bid_id, transfer.unbindle()); + swap.to_strict_serialized::<{ U32 }>() + .map_err(|err| TransferError::WrongConsig(err.to_string()))? + } else { + transfer + .to_strict_serialized::<{ U32 }>() + .map_err(|err| TransferError::WrongConsig(err.to_string()))? + }; let bp_txid = bp::Txid::from_hex(&psbt.to_txid().to_hex()) .map_err(|err| TransferError::WrongConsig(err.to_string()))?; @@ -721,8 +798,6 @@ async fn internal_transfer_asset( .map_err(|err| TransferError::WrongInvoice(err.to_string()))?; let contract_id = rgb_invoice.contract.unwrap().to_string(); - let consig_id = transfer.bindle_id().to_string(); - let rgb_transfer = RgbTransfer { iface: rgb_invoice.iface.unwrap().to_string(), consig_id: consig_id.clone(), @@ -757,6 +832,426 @@ async fn internal_transfer_asset( Ok(resp) } +#[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] +#[display(doc_comments)] +pub enum RgbSwapError { + /// Some request data is missing. {0:?} + Validation(BTreeMap), + /// Retrieve I/O or connectivity error. {0:?} + IO(RgbPersistenceError), + /// Watcher is required in this operation. Please, create watcher. + NoWatcher, + /// Contract is required in this operation. Please, import or issue a Contract. + NoContract, + /// Avaliable Utxo is required in this operation. {0} + NoUtxo(String), + /// The Offer has expired. + OfferExpired, + /// Insufficient funds (expected: {input} sats / current: {output} sats) + Inflation { + /// Amount spent: input amounts + input: u64, + + /// Amount sent: sum of output value + transaction fee + output: u64, + }, + /// Occurs an error in export step. {0} + Export(ExportContractError), + /// Occurs an error in create offer buyer step. {0} + Buyer(RgbOfferErrors), + /// Occurs an error in create step. {0} + Create(PsbtError), + /// Occurs an error in estimate fee step. {0} + Estimate(EstimateFeeError), + /// Occurs an error in publish offer step. {0} + Marketplace(RgbOfferErrors), + /// Occurs an error in invoice step. {0} + Invoice(InvoiceError), + /// Occurs an error in create offer swap step. {0} + Swap(RgbOfferErrors), + /// Occurs an error in transfer step. {0} + Transfer(TransferError), + /// Swap fee cannot be decoded. {0} + WrongSwapFee(String), + /// Bitcoin network cannot be decoded. {0} + WrongNetwork(String), + /// Bitcoin address cannot be decoded. {0} + WrongAddress(String), + /// Seller PSBT cannot be decoded. {0} + WrongPsbtSeller(String), + /// Buyer PSBT cannot be decoded. {0} + WrongPsbtBuyer(String), + /// PSBTs cannot be merged. {0} + WrongPsbtSwap(String), +} + +pub async fn create_seller_offer( + sk: &str, + request: RgbOfferRequest, +) -> Result { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .flatten() + .into_iter() + .map(|(f, e)| (f, 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 resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..Default::default() + }; + + let (mut stock, mut rgb_account) = + retrieve_stock_account(sk).await.map_err(RgbSwapError::IO)?; + + let mut rgb_wallet = match rgb_account.wallets.get(RGB_DEFAULT_NAME) { + Some(rgb_wallet) => rgb_wallet.to_owned(), + _ => return Err(RgbSwapError::NoWatcher), + }; + + let (allocations, asset_inputs, bitcoin_inputs, bitcoin_changes) = + prebuild_seller_swap(request.clone(), &mut stock, &mut rgb_wallet, &mut resolver).await?; + + rgb_account + .wallets + .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet.clone()); + + let RgbOfferRequest { + contract_id, + contract_amount, + bitcoin_price, + change_terminal, + iface, + expire_at, + .. + } = request; + + let psbt_req = PsbtRequest { + fee: PsbtFeeRequest::Value(0), + asset_inputs, + bitcoin_inputs, + bitcoin_changes, + asset_descriptor_change: None, + asset_terminal_change: Some(change_terminal), + }; + + let seller_psbt = internal_create_psbt( + psbt_req, + true, + Some(EcdsaSighashType::NonePlusAnyoneCanPay), + &mut rgb_account, + &mut resolver, + ) + .await + .map_err(RgbSwapError::Create)?; + + let iface_index = match iface.to_uppercase().as_str() { + "RGB20" => AssetType::RGB20, + "RGB21" => AssetType::RGB21, + _ => AssetType::Contract, + } as u32; + + let network = AddressNetwork::from(network); + + let seller_address = next_address(iface_index, rgb_wallet.clone(), network) + .map_err(|op| RgbSwapError::WrongAddress(op.to_string()))? + .address; + + let new_offer = RgbOffer::new( + sk.to_string(), + contract_id.clone(), + iface.clone(), + allocations, + seller_address, + bitcoin_price, + seller_psbt.psbt.clone(), + expire_at, + ); + + 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); + } + + 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) +} + +pub async fn create_buyer_bid( + sk: &str, + request: RgbBidRequest, +) -> Result { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .flatten() + .into_iter() + .map(|(f, e)| (f, e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } + + let mut resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..Default::default() + }; + + let (mut stock, mut rgb_account) = + retrieve_stock_account(sk).await.map_err(RgbSwapError::IO)?; + + let mut rgb_wallet = match rgb_account.wallets.get(RGB_DEFAULT_NAME) { + Some(rgb_wallet) => rgb_wallet.to_owned(), + _ => return Err(RgbSwapError::NoWatcher), + }; + + let RgbBidRequest { + offer_id, + change_terminal, + .. + } = request.clone(); + + let offer = get_public_offer(offer_id) + .await + .map_err(RgbSwapError::Buyer)?; + + let (mut new_bid, bitcoin_inputs, bitcoin_changes, fee_value) = + prebuild_buyer_swap(sk, request, &mut rgb_wallet, &mut resolver).await?; + + let buyer_outpoint = watcher_next_utxo(sk, "default", &offer.iface.to_uppercase()) + .await + .map_err(|op| RgbSwapError::NoUtxo(op.to_string()))?; + + let buyer_outpoint = if let Some(utxo) = buyer_outpoint.utxo { + utxo.outpoint.to_string() + } else { + return Err(RgbSwapError::NoUtxo(String::new())); + }; + + rgb_account + .wallets + .insert(RGB_DEFAULT_NAME.to_owned(), rgb_wallet.clone()); + + let psbt_req = PsbtRequest { + fee: PsbtFeeRequest::Value(fee_value), + asset_inputs: vec![], + bitcoin_inputs, + bitcoin_changes, + asset_descriptor_change: None, + asset_terminal_change: Some(change_terminal.clone()), + }; + + let buyer_psbt = internal_create_psbt( + psbt_req, + false, + Some(EcdsaSighashType::NonePlusAnyoneCanPay), + &mut rgb_account, + &mut resolver, + ) + .await + .map_err(RgbSwapError::Create)?; + + new_bid.buyer_psbt = buyer_psbt.psbt.clone(); + + let contract_id = &new_bid.contract_id; + let mut my_bids = retrieve_bids(sk).await.map_err(RgbSwapError::IO)?; + if let Some(bids) = my_bids.bids.get(contract_id) { + let mut current_bids = bids.to_owned(); + current_bids.push(new_bid.clone()); + my_bids.bids.insert(contract_id.clone(), current_bids); + } + + let seller_psbt = Psbt::from_str(&offer.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 seller_psbt = PartiallySignedTransaction::from(seller_psbt); + let buyer_psbt = PartiallySignedTransaction::from(buyer_psbt); + + let swap_psbt = seller_psbt + .join(buyer_psbt) + .map_err(|op| RgbSwapError::WrongPsbtSwap(op.to_string()))?; + + let swap_psbt = Psbt::from(swap_psbt); + let swap_psbt = Serialize::serialize(&swap_psbt).to_hex(); + + let RgbOfferSwap { + iface, + public: offer_pub, + expire_at, + .. + } = offer.clone(); + + let RgbBid { + bid_id, + offer_id, + asset_amount, + .. + } = 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_req = InvoiceRequest { + iface, + contract_id: contract_id.to_string(), + amount: asset_amount, + seal: format!("tapret1st:{buyer_outpoint}"), + params: HashMap::new(), + }; + let invoice = internal_create_invoice(invoice_req, &mut stock) + .await + .map_err(RgbSwapError::Invoice)?; + + let invoice = invoice.to_string(); + new_bid.buyer_invoice = invoice.clone(); + + let resp = RgbBidResponse { + bid_id, + offer_id, + invoice, + swap_psbt, + 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) +} + +pub async fn create_swap_transfer( + sk: &str, + request: RgbSwapRequest, +) -> Result { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .flatten() + .into_iter() + .map(|(f, e)| (f, e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } + + let (mut stock, mut rgb_account, mut rgb_transfers) = retrieve_stock_account_transfers(sk) + .await + .map_err(RgbSwapError::IO)?; + + let RgbSwapRequest { + offer_id, + bid_id, + swap_psbt, + .. + } = request.clone(); + + let RgbOfferSwap { + iface, expire_at, .. + } = get_public_offer(offer_id.clone()) + .await + .map_err(RgbSwapError::Swap)?; + + let RgbBidSwap { buyer_invoice, .. } = + get_swap_bid(sk, offer_id.clone(), bid_id.clone(), expire_at) + .await + .map_err(RgbSwapError::Swap)?; + + let change_terminal = match iface.to_uppercase().as_str() { + "RGB20" => "/20/1", + "RGB21" => "/21/1", + _ => "/10/1", + }; + + let transfer_req = RgbTransferRequest { + psbt: swap_psbt, + rgb_invoice: buyer_invoice.to_string(), + terminal: change_terminal.to_string(), + }; + + let params = RgbTransferInternalParams { + offer_id: Some(offer_id.clone()), + bid_id: Some(bid_id), + }; + + let RgbTransferResponse { + consig_id, + consig: final_consig, + psbt: final_psbt, + .. + } = internal_transfer_asset( + transfer_req, + params, + &mut stock, + &mut rgb_account, + &mut rgb_transfers, + ) + .await + .map_err(RgbSwapError::Transfer)?; + + let mut my_offers = retrieve_offers(sk).await.map_err(RgbSwapError::IO)?; + mark_transfer_offer(offer_id, consig_id.clone(), &mut my_offers) + .await + .map_err(RgbSwapError::Swap)?; + + store_offers(sk, my_offers) + .await + .map_err(RgbSwapError::IO)?; + + store_stock_account_transfers(sk, stock, rgb_account, rgb_transfers) + .await + .map_err(RgbSwapError::IO)?; + + Ok(RgbSwapResponse { + consig_id, + final_consig, + final_psbt, + }) +} + pub async fn accept_transfer( sk: &str, request: AcceptRequest, @@ -803,6 +1298,10 @@ pub enum SaveTransferError { IO(RgbPersistenceError), /// Occurs an error in parse consig step. {0} WrongConsig(AcceptTransferError), + /// Occurs an error in parse consig swap step. {0} + WrongConsigSwap(TransferSwapError), + /// Occurs an error in swap step. {0} + WrongSwap(RgbOfferErrors), /// Write I/O or connectivity error. {1} in {0} Write(String, String), } @@ -826,23 +1325,24 @@ pub async fn save_transfer( .await .map_err(SaveTransferError::IO)?; - let (txid, transfer) = extract_transfer(consignment).map_err(SaveTransferError::WrongConsig)?; - - let consig = transfer - .to_strict_serialized::<{ U32 }>() - .map_err(|err| TransferError::WrongConsig(err.to_string())) - .map_err(|_| SaveTransferError::WrongConsig(AcceptTransferError::WrongHex))?; + let RgbExtractTransfer { + consig_id, + contract_id, + tx_id, + strict, + offer_id, + bid_id, + .. + } = prebuild_extract_transfer(&consignment)?; - let consig_id = transfer.bindle_id().to_string(); let rgb_transfer = RgbTransfer { consig_id: consig_id.clone(), - consig: consig.to_hex(), + consig: strict.to_hex(), iface, - tx: txid, + tx: tx_id, is_send: false, }; - let contract_id = transfer.contract_id().to_string(); if let Some(transfers) = rgb_transfers.transfers.get(&contract_id.clone()) { let mut new_transfer = transfers.to_owned(); new_transfer.push(rgb_transfer); @@ -855,12 +1355,32 @@ pub async fn save_transfer( .insert(contract_id.clone(), vec![rgb_transfer]); } + let mut status = BTreeMap::new(); + status.insert(consig_id.clone(), false); + store_transfers(sk, rgb_transfers) .await .map_err(SaveTransferError::IO)?; - let mut status = BTreeMap::new(); - status.insert(consig_id, false); + if let Some(offer_id) = offer_id { + let mut my_offers = retrieve_offers(sk).await.map_err(SaveTransferError::IO)?; + mark_transfer_offer(offer_id.to_string(), consig_id.clone(), &mut my_offers) + .await + .map_err(SaveTransferError::WrongSwap)?; + store_offers(sk, my_offers) + .await + .map_err(SaveTransferError::IO)?; + }; + + if let Some(bid_id) = bid_id { + let mut my_bids = retrieve_bids(sk).await.map_err(SaveTransferError::IO)?; + mark_transfer_bid(bid_id.to_string(), consig_id, &mut my_bids) + .await + .map_err(SaveTransferError::WrongSwap)?; + store_bids(sk, my_bids) + .await + .map_err(SaveTransferError::IO)?; + }; Ok(RgbTransferStatusResponse { contract_id, @@ -918,6 +1438,65 @@ pub async fn verify_transfers(sk: &str) -> Result = transfers + .clone() + .into_iter() + .filter(|x| x.is_mine) + .map(|x| x.consig_id) + .collect(); + + let check_bids: Vec<_> = transfers + .clone() + .into_iter() + .filter(|x| x.is_mine) + .map(|x| x.consig_id) + .collect(); + + if !check_offers.is_empty() { + let mut my_offers = retrieve_offers(sk).await.map_err(TransferError::IO)?; + for transfer_id in check_offers { + if let Some(offer) = mark_offer_fill(transfer_id, &mut my_offers) + .await + .map_err(TransferError::WrongSwap)? + { + my_public_offers.push(offer); + } + } + store_offers(sk, my_offers) + .await + .map_err(TransferError::IO)?; + } + + if !check_bids.is_empty() { + let mut my_bids = retrieve_bids(sk).await.map_err(TransferError::IO)?; + for transfer_id in check_bids { + mark_bid_fill(transfer_id, &mut my_bids) + .await + .map_err(TransferError::WrongSwap)?; + } + store_bids(sk, my_bids).await.map_err(TransferError::IO)?; + } + + if !my_public_offers.is_empty() { + remove_public_offers(my_public_offers) + .await + .map_err(TransferError::WrongSwap)?; + } + + store_stock_transfers(sk, stock, rgb_pending) + .await + .map_err(TransferError::IO)?; + + Ok(BatchRgbTransferResponse { transfers }) +} + +pub async fn internal_verify_transfers( + stock: &mut Stock, + rgb_transfers: RgbTransfers, +) -> Result<(RgbTransfers, Vec), TransferError> { let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() @@ -946,7 +1525,7 @@ pub async fn verify_transfers(sk: &str) -> Result { prefetch_resolver_rgb(&activity.consig, &mut resolver, None).await; - accept_rgb_transfer(activity.consig.clone(), false, &mut resolver, &mut stock) + accept_rgb_transfer(activity.consig.clone(), false, &mut resolver, stock) .map_err(TransferError::Accept)? } _ => { @@ -957,6 +1536,7 @@ pub async fn verify_transfers(sk: &str) -> Result Result Result Result Result { @@ -1195,6 +1773,74 @@ pub async fn list_transfers(sk: &str, contract_id: String) -> Result Result { + let rgb_offers = retrieve_offers(sk).await?; + let rgb_bids = retrieve_bids(sk).await?; + + let mut offers = vec![]; + rgb_offers + .offers + .into_iter() + .for_each(|(_, offs)| offers.extend(offs.into_iter().map(RgbOfferDetail::from))); + + let mut bids = vec![]; + rgb_bids + .bids + .into_iter() + .for_each(|(_, bs)| bids.extend(bs.into_iter().map(RgbBidDetail::from))); + + Ok(RgbOfferBidsResponse { offers, bids }) +} + +pub async fn list_my_offers(sk: &str) -> Result { + let rgb_offers = retrieve_offers(sk).await?; + + let mut offers = vec![]; + rgb_offers + .offers + .into_iter() + .for_each(|(_, offs)| offers.extend(offs.into_iter().map(RgbOfferDetail::from))); + + Ok(RgbOffersResponse { offers }) +} + +pub async fn list_my_bids(sk: &str) -> Result { + let rgb_bids = retrieve_bids(sk).await?; + let mut bids = vec![]; + rgb_bids + .bids + .into_iter() + .for_each(|(_, bs)| bids.extend(bs.into_iter().map(RgbBidDetail::from))); + + Ok(RgbBidsResponse { bids }) +} + +pub async fn list_public_offers(_sk: &str) -> Result { + let rgb_public_offers = retrieve_public_offers().await?; + + let mut offers = vec![]; + let mut bids = BTreeMap::new(); + rgb_public_offers + .rgb_offers + .offers + .into_iter() + .for_each(|(_, offs)| offers.extend(offs.into_iter().map(PublicRgbOfferResponse::from))); + + rgb_public_offers + .rgb_offers + .bids + .into_iter() + .for_each(|(offer_id, bs)| { + let bs = bs + .values() + .map(|x| PublicRgbBidResponse::from(x.to_owned())) + .collect(); + bids.insert(offer_id, bs); + }); + + Ok(PublicRgbOffersResponse { offers, bids }) +} + #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum ImportError { @@ -1563,7 +2209,7 @@ pub async fn watcher_unspent_utxos( let iface_index = match iface { "RGB20" => 20, "RGB21" => 21, - _ => 9, + _ => 10, }; let mut resolver = ExplorerResolver { diff --git a/src/rgb/carbonado.rs b/src/rgb/carbonado.rs index 2690d04c..704eb662 100644 --- a/src/rgb/carbonado.rs +++ b/src/rgb/carbonado.rs @@ -5,25 +5,34 @@ use postcard::{from_bytes, to_allocvec}; use rgbstd::{persistence::Stock, stl::LIB_ID_RGB}; use strict_encoding::{StrictDeserialize, StrictSerialize}; -use crate::rgb::crdt::LocalRgbAccount; -use crate::rgb::crdt::RawRgbAccount; -use crate::rgb::structs::RgbTransfers; +use crate::carbonado::server_store; +use crate::rgb::crdt::{LocalRgbAccount, LocalRgbOffers, RawRgbAccount}; + +use crate::rgb::{ + structs::RgbTransfers, + swap::{RgbBids, RgbOffers}, +}; use crate::{ - carbonado::{retrieve, store}, + carbonado::{retrieve, server_retrieve, store}, rgb::{constants::RGB_STRICT_TYPE_VERSION, structs::RgbAccount}, }; +use super::crdt::LocalRgbOfferBid; +use super::swap::{PublicRgbOffers, RgbBidSwap}; + #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum StorageError { /// File '{0}' retrieve causes error. {1} FileRetrieve(String, String), /// File '{0}' write causes error. {1} - WriteRetrieve(String, String), + FileWrite(String, String), /// Changes '{0}' retrieve causes error. {1} ChangesRetrieve(String, String), /// Changes '{0}' write causes error. {1} ChangesWrite(String, String), + /// Fork '{0}' read causes error. {1} + ForkRead(String, String), /// Fork '{0}' write causes error. {1} ForkWrite(String, String), /// Merge '{0}' write causes error. {1} @@ -44,6 +53,7 @@ pub enum StorageError { Reconcile(String, String), } +// User Carbonado Operations pub async fn store_stock(sk: &str, name: &str, stock: &Stock) -> Result<(), StorageError> { let data = stock .to_strict_serialized::() @@ -110,6 +120,48 @@ pub async fn store_transfers( .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) } +pub async fn store_offers( + sk: &str, + name: &str, + rgb_offers: &RgbOffers, +) -> Result<(), StorageError> { + let data = to_allocvec(rgb_offers) + .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; + + let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + store( + sk, + &format!("{hashed_name}.c15"), + &data, + true, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) +} + +pub async fn store_bids(sk: &str, name: &str, rgb_bids: &RgbBids) -> Result<(), StorageError> { + let data = to_allocvec(rgb_bids) + .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; + + let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + store( + sk, + &format!("{hashed_name}.c15"), + &data, + true, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) +} + pub async fn retrieve_stock(sk: &str, name: &str) -> Result { let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) .to_hex() @@ -167,6 +219,43 @@ pub async fn retrieve_transfers(sk: &str, name: &str) -> Result Result { + let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + let (data, _) = retrieve(sk, &format!("{hashed_name}.c15"), vec![]) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + + if data.is_empty() { + Ok(RgbOffers::default()) + } else { + let rgb_offers = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + Ok(rgb_offers) + } +} + +pub async fn retrieve_bids(sk: &str, name: &str) -> Result { + let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + let (data, _) = retrieve(sk, &format!("{hashed_name}.c15"), vec![]) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + + if data.is_empty() { + Ok(RgbBids::default()) + } else { + let rgb_bids = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + Ok(rgb_bids) + } +} + +// CDRT Operations pub async fn store_fork_wallets(sk: &str, name: &str, changes: &[u8]) -> Result<(), StorageError> { let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) .to_hex() @@ -206,7 +295,7 @@ pub async fn store_fork_wallets(sk: &str, name: &str, changes: &[u8]) -> Result< let mut latest_version = automerge::AutoCommit::new(); reconcile(&mut latest_version, raw_merged) - .map_err(|op| StorageError::WriteRetrieve(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::FileWrite(name.to_string(), op.to_string()))?; let data = to_allocvec(&merged) .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; @@ -269,3 +358,178 @@ pub async fn retrieve_fork_wallets(sk: &str, name: &str) -> 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, _) = server_retrieve(main_name) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + if data.is_empty() { + Ok(LocalRgbOffers { + doc: automerge::AutoCommit::new().save(), + rgb_offers: PublicRgbOffers::default(), + }) + } else { + let mut original_version = automerge::AutoCommit::new(); + let rgb_offers: PublicRgbOffers = 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(); + + server_store( + 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(LocalRgbOffers { + doc: fork_version.save(), + rgb_offers, + }) + } +} + +pub async fn store_public_offers(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, _) = server_retrieve(original_name) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + + let mut original_version = automerge::AutoCommit::load(&original_bytes) + .map_err(|op| StorageError::ForkRead(name.to_string(), op.to_string()))?; + + let mut fork_version = automerge::AutoCommit::load(changes) + .map_err(|op| StorageError::ChangesRetrieve(name.to_string(), op.to_string()))?; + + original_version + .merge(&mut fork_version) + .map_err(|op| StorageError::MergeWrite(name.to_string(), op.to_string()))?; + + let public_offers: PublicRgbOffers = 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())) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(()) +} + +pub async fn retrieve_swap_offer_bid( + sk: &str, + name: &str, + expire_at: Option, +) -> Result { + let mut hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + if let Some(expire_at) = expire_at { + hashed_name = format!("{hashed_name}-{expire_at}"); + } + + let main_name = &format!("{hashed_name}.c15"); + let original_name = &format!("{hashed_name}-diff.c15"); + + let (data, _) = retrieve(sk, main_name, vec![]) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + if data.is_empty() { + Ok(LocalRgbOfferBid { + doc: automerge::AutoCommit::new().save(), + rgb_bid: RgbBidSwap::default(), + }) + } else { + let mut original_version = automerge::AutoCommit::new(); + let rgb_offer_bid: RgbBidSwap = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + + reconcile(&mut original_version, rgb_offer_bid.clone()) + .map_err(|op| StorageError::Reconcile(name.to_string(), op.to_string()))?; + + let mut fork_version = original_version.fork(); + + store( + sk, + original_name, + &fork_version.save(), + false, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(LocalRgbOfferBid { + doc: fork_version.save(), + rgb_bid: rgb_offer_bid, + }) + } +} + +pub async fn store_swap_offer_bid( + sk: &str, + name: &str, + changes: &[u8], + expire_at: Option, +) -> Result<(), StorageError> { + let mut hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) + .to_hex() + .to_lowercase(); + + if let Some(expire_at) = expire_at { + hashed_name = format!("{hashed_name}-{expire_at}"); + } + + let main_name = &format!("{hashed_name}.c15"); + let original_name = &format!("{hashed_name}-diff.c15"); + + let (original_bytes, _) = server_retrieve(original_name) + .await + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + + let mut original_version = automerge::AutoCommit::load(&original_bytes) + .map_err(|op| StorageError::ForkRead(name.to_string(), op.to_string()))?; + + let mut fork_version = automerge::AutoCommit::load(changes) + .map_err(|op| StorageError::ChangesRetrieve(name.to_string(), op.to_string()))?; + + original_version + .merge(&mut fork_version) + .map_err(|op| StorageError::MergeWrite(name.to_string(), op.to_string()))?; + + let rgb_bid: RgbBidSwap = hydrate(&original_version).unwrap(); + + let data = to_allocvec(&rgb_bid) + .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; + + store( + sk, + main_name, + &data, + false, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(()) +} diff --git a/src/rgb/crdt.rs b/src/rgb/crdt.rs index 076fb4c2..1fffbba8 100644 --- a/src/rgb/crdt.rs +++ b/src/rgb/crdt.rs @@ -12,6 +12,8 @@ use std::{ use crate::rgb::structs::RgbAccount; +use super::swap::{PublicRgbOffers, RgbBidSwap}; + #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum RgbMergeError { @@ -284,3 +286,17 @@ pub struct LocalCopyData { pub doc: Vec, pub data: Vec, } + +#[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] +#[display(doc_comments)] +pub struct LocalRgbOffers { + pub doc: Vec, + pub rgb_offers: PublicRgbOffers, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] +#[display(doc_comments)] +pub struct LocalRgbOfferBid { + pub doc: Vec, + pub rgb_bid: RgbBidSwap, +} diff --git a/src/rgb/fs.rs b/src/rgb/fs.rs index d3a34fb8..c6b47f10 100644 --- a/src/rgb/fs.rs +++ b/src/rgb/fs.rs @@ -1,11 +1,22 @@ use rgbstd::persistence::Stock; -use crate::constants::storage_keys::{ASSETS_STOCK, ASSETS_TRANSFERS, ASSETS_WALLETS}; +use super::carbonado::store_swap_offer_bid; +use super::crdt::{LocalRgbOfferBid, LocalRgbOffers}; +use super::swap::{RgbBids, RgbOffers}; +use crate::constants::storage_keys::{ + ASSETS_BIDS, ASSETS_OFFERS, ASSETS_STOCK, ASSETS_TRANSFERS, ASSETS_WALLETS, MARKETPLACE_OFFERS, +}; + use crate::rgb::{ carbonado::{ - retrieve_fork_wallets, retrieve_stock as retrieve_rgb_stock, - retrieve_transfers as retrieve_rgb_transfers, retrieve_wallets, store_fork_wallets, - store_stock as store_rgb_stock, store_transfers as store_rgb_transfer, store_wallets, + retrieve_bids as retrieve_rgb_bids, retrieve_fork_wallets, + 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_fork_wallets, store_offers as store_rgb_offers, + store_public_offers as store_rgb_public_offers, store_stock as store_rgb_stock, + store_transfers as store_rgb_transfer, store_wallets, }, crdt::LocalRgbAccount, structs::{RgbAccount, RgbTransfers}, @@ -22,6 +33,14 @@ pub enum RgbPersistenceError { RetrieveRgbAccountFork(String), // Retrieve Transfers Error. {0} RetrieveRgbTransfers(String), + // Retrieve Offers Error. {0} + RetrieveRgbOffers(String), + // Retrieve Bids Error. {0} + RetrieveRgbBids(String), + // Retrieve Swap Bid Error. {0} + RetrieveSwapBids(String), + // Retrieve Public Offers Error. {0} + RetrievePublicOffers(String), // Store Stock Error. {0} WriteStock(String), // Store RgbAccount Error. {0} @@ -30,6 +49,14 @@ pub enum RgbPersistenceError { WriteRgbAccountFork(String), // Store Transfers Error. {0} WriteRgbTransfers(String), + // Store Offers Error. {0} + WriteRgbOffers(String), + // Store Bids Error. {0} + WriteRgbBids(String), + // Store Public Offers Error. {0} + WriteRgbPublicOffers(String), + // Store Swap Bid Error. {0} + WriteSwapBids(String), } pub async fn retrieve_stock(sk: &str) -> Result { @@ -64,6 +91,42 @@ pub async fn retrieve_local_account(sk: &str) -> Result Result { + let stock = retrieve_rgb_public_offers(MARKETPLACE_OFFERS) + .await + .map_err(|op| RgbPersistenceError::RetrievePublicOffers(op.to_string()))?; + + Ok(stock) +} + +pub async fn retrieve_swap_offer_bid( + sk: &str, + name: &str, + expire_at: Option, +) -> Result { + let stock = retrieve_rgb_swap_offer_bid(sk, name, expire_at) + .await + .map_err(|op| RgbPersistenceError::RetrieveSwapBids(op.to_string()))?; + + Ok(stock) +} + +pub async fn retrieve_offers(sk: &str) -> Result { + let offers = retrieve_rgb_offers(sk, ASSETS_OFFERS) + .await + .map_err(|op| RgbPersistenceError::RetrieveRgbOffers(op.to_string()))?; + + Ok(offers) +} + +pub async fn retrieve_bids(sk: &str) -> Result { + let bids = retrieve_rgb_bids(sk, ASSETS_BIDS) + .await + .map_err(|op| RgbPersistenceError::RetrieveRgbBids(op.to_string()))?; + + Ok(bids) +} + pub async fn retrieve_stock_account(sk: &str) -> Result<(Stock, RgbAccount), RgbPersistenceError> { Ok((retrieve_stock(sk).await?, retrieve_account(sk).await?)) } @@ -108,6 +171,35 @@ pub async fn store_local_account(sk: &str, changes: Vec) -> Result<(), RgbPe .map_err(|op| RgbPersistenceError::WriteRgbAccountFork(op.to_string())) } +pub async fn store_offers(sk: &str, rgb_offers: RgbOffers) -> Result<(), RgbPersistenceError> { + store_rgb_offers(sk, ASSETS_OFFERS, &rgb_offers) + .await + .map_err(|op| RgbPersistenceError::WriteRgbOffers(op.to_string())) +} + +pub async fn store_bids(sk: &str, rgb_bids: RgbBids) -> Result<(), RgbPersistenceError> { + store_rgb_bids(sk, ASSETS_BIDS, &rgb_bids) + .await + .map_err(|op| RgbPersistenceError::WriteRgbBids(op.to_string())) +} + +pub async fn store_swap_bids( + sk: &str, + name: &str, + changes: Vec, + expire_at: Option, +) -> Result<(), RgbPersistenceError> { + store_swap_offer_bid(sk, name, &changes, expire_at) + .await + .map_err(|op| RgbPersistenceError::WriteSwapBids(op.to_string())) +} + +pub async fn store_public_offers(changes: Vec) -> Result<(), RgbPersistenceError> { + store_rgb_public_offers(MARKETPLACE_OFFERS, &changes) + .await + .map_err(|op| RgbPersistenceError::WriteRgbPublicOffers(op.to_string())) +} + pub async fn store_stock_account( sk: &str, stock: Stock, diff --git a/src/rgb/prebuild.rs b/src/rgb/prebuild.rs index 5c5411e9..85a40570 100644 --- a/src/rgb/prebuild.rs +++ b/src/rgb/prebuild.rs @@ -1,5 +1,7 @@ -use std::{collections::BTreeMap, str::FromStr}; +use std::{collections::BTreeMap, ops::Mul, str::FromStr}; +use amplify::{confinement::Confined, hex::FromHex}; +use bech32::{decode, FromBase32}; use bitcoin::Network; use bitcoin_scripts::address::AddressNetwork; use garde::Validate; @@ -14,10 +16,11 @@ use rgbwallet::RgbInvoice; use strict_encoding::tn; use crate::{ - constants::NETWORK, + bitcoin::get_swap_new_address, + constants::{get_marketplace_fee_percentage, NETWORK}, structs::{ AllocationDetail, AllocationValue, AssetType, FullRgbTransferRequest, PsbtFeeRequest, - PsbtInputRequest, SecretString, + PsbtInputRequest, RgbBidRequest, RgbOfferRequest, SecretString, }, validators::RGBContext, }; @@ -26,19 +29,21 @@ use crate::rgb::{ constants::{BITCOIN_DEFAULT_FETCH_LIMIT, RGB_DEFAULT_FETCH_LIMIT}, contract::export_contract, fs::RgbPersistenceError, + prefetch::prefetch_resolver_txs, prefetch::{ prefetch_resolver_allocations, prefetch_resolver_user_utxo_status, prefetch_resolver_utxos, }, psbt::estimate_fee_tx, resolvers::ExplorerResolver, structs::AddressAmount, + structs::RgbExtractTransfer, + swap::{extract_transfer as extract_swap_transfer, get_public_offer, RgbBid, RgbOfferSwap}, + transfer::extract_transfer, wallet::sync_wallet, wallet::{get_address, next_utxos}, - TransferError, + RgbSwapError, SaveTransferError, TransferError, }; -use super::prefetch::prefetch_resolver_txs; - pub const DUST_LIMIT_SATOSHI: u64 = 546; pub async fn prebuild_transfer_asset( @@ -364,6 +369,7 @@ pub async fn prebuild_transfer_asset( .map_err(|err| TransferError::WrongNetwork(err.to_string()))?; let network = AddressNetwork::from(network); + // TODO: Use New Address let change_address = get_address(1, 0, rgb_wallet.clone(), network) .map_err(|err| TransferError::WrongNetwork(err.to_string()))? .address; @@ -374,3 +380,509 @@ pub async fn prebuild_transfer_asset( Ok((assets_inputs, bitcoin_inputs, bitcoin_changes, fee_value)) } + +pub async fn prebuild_seller_swap( + request: RgbOfferRequest, + stock: &mut Stock, + rgb_wallet: &mut RgbWallet, + resolver: &mut ExplorerResolver, +) -> Result< + ( + Vec, + Vec, + Vec, + Vec, + ), + RgbSwapError, +> { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .flatten() + .into_iter() + .map(|(f, e)| (f, e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } + + let contract_id = ContractId::from_str(&request.contract_id).map_err(|_| { + let mut errors = BTreeMap::new(); + errors.insert("contract_id".to_string(), "invalid contract id".to_string()); + RgbSwapError::Validation(errors) + })?; + + let RgbOfferRequest { + descriptor, + iface: iface_name, + contract_amount: target_amount, + mut bitcoin_changes, + .. + } = request; + + let wildcard_terminal = "/*/*"; + let mut universal_desc = descriptor.to_string(); + for contract_type in [ + AssetType::RGB20, + AssetType::RGB21, + AssetType::Contract, + AssetType::Bitcoin, + AssetType::Change, + ] { + let contract_index = contract_type as u32; + let terminal_step = format!("/{contract_index}/*"); + if universal_desc.contains(&terminal_step) { + universal_desc = universal_desc.replace(&terminal_step, wildcard_terminal); + break; + } + } + + let universal_desc = SecretString(universal_desc); + let mut all_unspents = vec![]; + + // Get All Assets UTXOs + let contract_index = if let "RGB20" = iface_name.as_str() { + AssetType::RGB20 + } else { + AssetType::RGB21 + }; + + let iface = stock + .iface_by_name(&tn!(iface_name)) + .map_err(|_| RgbSwapError::NoContract)?; + let contract_iface = stock + .contract_iface(contract_id, iface.iface_id()) + .map_err(|_| RgbSwapError::NoContract)?; + + let contract_index = contract_index as u32; + sync_wallet(contract_index, rgb_wallet, resolver); + prefetch_resolver_utxos( + contract_index, + rgb_wallet, + resolver, + Some(RGB_DEFAULT_FETCH_LIMIT), + ) + .await; + prefetch_resolver_allocations(contract_iface, resolver).await; + + let contract = export_contract(contract_id, stock, resolver, &mut Some(rgb_wallet.clone())) + .map_err(RgbSwapError::Export)?; + + let allocations: Vec = contract + .allocations + .into_iter() + .filter(|x| x.is_mine && !x.is_spent) + .collect(); + + let asset_total: u64 = allocations + .clone() + .into_iter() + .filter(|a| a.is_mine && !a.is_spent) + .map(|a| match a.value { + AllocationValue::Value(value) => value.to_owned(), + AllocationValue::UDA(_) => 1, + }) + .sum(); + + if asset_total < target_amount { + let mut errors = BTreeMap::new(); + errors.insert("contract".to_string(), "insufficient state".to_string()); + return Err(RgbSwapError::Validation(errors)); + } + + let asset_unspent_utxos = &mut next_utxos(contract_index, rgb_wallet.clone(), resolver) + .map_err(|_| RgbSwapError::IO(RgbPersistenceError::RetrieveRgbAccount("".to_string())))?; + + let mut asset_total = 0; + let mut assets_inputs = vec![]; + let mut assets_allocs = vec![]; + + let mut rng = StdRng::from_entropy(); + let rnd_amount = rng.gen_range(600..1500); + let mut total_asset_bitcoin_unspend: u64 = 0; + for alloc in allocations.iter() { + match alloc.value { + AllocationValue::Value(alloc_value) => { + if asset_total >= target_amount { + break; + } + + let input = PsbtInputRequest { + descriptor: universal_desc.clone(), + utxo: alloc.utxo.clone(), + utxo_terminal: alloc.derivation.to_string(), + tapret: None, + }; + if !assets_inputs + .clone() + .into_iter() + .any(|x: PsbtInputRequest| x.utxo == alloc.utxo) + { + assets_inputs.push(input); + assets_allocs.push(alloc.clone()); + total_asset_bitcoin_unspend += asset_unspent_utxos + .clone() + .into_iter() + .filter(|x| { + x.outpoint.to_string() == alloc.utxo.clone() + && alloc.is_mine + && !alloc.is_spent + }) + .map(|x| x.amount) + .sum::(); + asset_total += alloc_value; + } + } + AllocationValue::UDA(_) => { + let input = PsbtInputRequest { + descriptor: universal_desc.clone(), + utxo: alloc.utxo.clone(), + utxo_terminal: alloc.derivation.to_string(), + tapret: None, + }; + if !assets_inputs + .clone() + .into_iter() + .any(|x| x.utxo == alloc.utxo) + { + assets_inputs.push(input); + assets_allocs.push(alloc.clone()); + total_asset_bitcoin_unspend += asset_unspent_utxos + .clone() + .into_iter() + .filter(|x| { + x.outpoint.to_string() == alloc.utxo.clone() + && alloc.is_mine + && !alloc.is_spent + }) + .map(|x| x.amount) + .sum::(); + } + break; + } + } + } + + // 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); + } + + let mut bitcoin_total = total_asset_bitcoin_unspend; + let total_spendable = rnd_amount + total_bitcoin_spend; + + 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}"), + 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, + }); + } else if change_value > DUST_LIMIT_SATOSHI { + let network = NETWORK.read().await.to_string(); + let network = Network::from_str(&network) + .map_err(|err| RgbSwapError::WrongNetwork(err.to_string()))?; + + let network = AddressNetwork::from(network); + // TODO: Use New Address + let change_address = get_address(1, 0, rgb_wallet.clone(), network) + .map_err(|err| RgbSwapError::WrongNetwork(err.to_string()))? + .address; + + let change_bitcoin = format!("{change_address}:{change_value}"); + bitcoin_changes.push(change_bitcoin); + } + + Ok(( + assets_allocs, + assets_inputs, + bitcoin_inputs, + bitcoin_changes, + )) +} + +pub async fn prebuild_buyer_swap( + sk: &str, + request: RgbBidRequest, + rgb_wallet: &mut RgbWallet, + resolver: &mut ExplorerResolver, +) -> Result<(RgbBid, Vec, Vec, u64), RgbSwapError> { + if let Err(err) = request.validate(&RGBContext::default()) { + let errors = err + .flatten() + .into_iter() + .map(|(f, e)| (f, e.to_string())) + .collect(); + return Err(RgbSwapError::Validation(errors)); + } + + let RgbBidRequest { + descriptor, + offer_id, + fee, + asset_amount, + .. + } = request; + + let wildcard_terminal = "/*/*"; + let mut universal_desc = descriptor.to_string(); + for contract_type in [AssetType::Bitcoin, AssetType::Change] { + let contract_index = contract_type as u32; + let terminal_step = format!("/{contract_index}/*"); + if universal_desc.contains(&terminal_step) { + universal_desc = universal_desc.replace(&terminal_step, wildcard_terminal); + break; + } + } + + let universal_desc = SecretString(universal_desc); + let mut all_unspents = vec![]; + + // Retrieve Offer + let offer = get_public_offer(offer_id.clone()) + .await + .map_err(RgbSwapError::Buyer)?; + + // Retrieve Bitcoin UTXOs + let mut bitcoin_inputs = vec![]; + + let only_bitcoin = [AssetType::Bitcoin, AssetType::Change]; + let derive_indexes = [ + AssetType::Bitcoin, + AssetType::Change, + AssetType::RGB20, + AssetType::RGB21, + ]; + for derive_type in derive_indexes { + let derive_index = derive_type.clone() as u32; + sync_wallet(derive_index, rgb_wallet, resolver); + prefetch_resolver_utxos( + derive_index, + rgb_wallet, + resolver, + Some(BITCOIN_DEFAULT_FETCH_LIMIT), + ) + .await; + prefetch_resolver_user_utxo_status(derive_index, rgb_wallet, resolver, false).await; + + if only_bitcoin.contains(&derive_type) { + let mut unspent_utxos = next_utxos(derive_index, rgb_wallet.clone(), resolver) + .map_err(|_| { + RgbSwapError::IO(RgbPersistenceError::RetrieveRgbAccount("".to_string())) + })?; + + all_unspents.append(&mut unspent_utxos); + } + } + + let RgbOfferSwap { + seller_address, + bitcoin_price, + .. + } = offer; + + let offer_change = format!("{seller_address}:{bitcoin_price}"); + let mut bitcoin_changes = vec![offer_change]; + + let mut bitcoin_total = 0; + let mut total_spendable = offer.bitcoin_price; + + // Swap Fee + if let Some(swap_fee_address) = get_swap_new_address() + .await + .map_err(|op| RgbSwapError::WrongSwapFee(op.to_string()))? + { + let swap_fee_perc = get_marketplace_fee_percentage().await; + let swap_fee_perc = if swap_fee_perc.is_empty() { + 0 + } else { + swap_fee_perc + .parse() + .map_err(|_| RgbSwapError::WrongSwapFee(swap_fee_perc))? + }; + let total_swap_fee = offer.bitcoin_price.mul(swap_fee_perc) / 100; + + bitcoin_changes.push(format!("{swap_fee_address}:{total_swap_fee}")); + total_spendable += total_swap_fee; + } + + // Bitcoin Fees + let (_, fee_value) = match fee.clone() { + PsbtFeeRequest::Value(fee_value) => { + let total_spendable = fee_value + total_spendable; + 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}"), + 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; + (change_value, fee_value) + } + PsbtFeeRequest::FeeRate(fee_rate) => { + // Increase dust limit to avoid dust change + let total_spendable = total_spendable + DUST_LIMIT_SATOSHI; + for utxo in all_unspents { + if total_spendable < bitcoin_total { + 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}"), + 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 txids = bitcoin_inputs + .clone() + .into_iter() + .map(|x| bitcoin::Txid::from_str(&x.utxo[..64]).expect("wrong txid")) + .collect(); + prefetch_resolver_txs(txids, resolver).await; + + let (change_value, fee) = estimate_fee_tx( + vec![], + bitcoin_inputs.clone(), + bitcoin_changes.clone(), + fee_rate, + rgb_wallet, + Some(0), + None, + resolver, + ) + .map_err(RgbSwapError::Estimate)?; + + (change_value, fee) + } + }; + + let total_spendable = fee_value + offer.bitcoin_price; + if bitcoin_total < total_spendable { + return Err(RgbSwapError::Inflation { + input: bitcoin_total, + output: total_spendable, + }); + } + + let bitcoin_utxos = bitcoin_inputs.clone().into_iter().map(|x| x.utxo).collect(); + let new_bid = RgbBid::new( + sk.to_string(), + offer_id, + offer.contract_id.clone(), + asset_amount, + offer.bitcoin_price, + bitcoin_utxos, + ); + + Ok((new_bid, bitcoin_inputs, bitcoin_changes, fee_value)) +} + +pub fn prebuild_extract_transfer( + consignment: &str, +) -> Result { + let serialized = if consignment.starts_with("rgb1") { + let (_, serialized, _) = + decode(consignment).expect("invalid serialized contract/genesis (bech32m format)"); + Vec::::from_base32(&serialized) + .expect("invalid hexadecimal contract/genesis (bech32m format)") + } else { + Vec::::from_hex(consignment).expect("invalid hexadecimal contract/genesis") + }; + + let confined = Confined::try_from_iter(serialized.iter().copied()).expect(""); + let (tx_id, transfer, offer_id, bid_id) = match extract_transfer(consignment.to_owned()) { + Ok((txid, tranfer)) => (txid, tranfer, None, None), + _ => match extract_swap_transfer(consignment.to_owned()) { + Ok((txid, tranfer, offer_id, bid_id)) => ( + txid, + tranfer, + Some(offer_id.to_string()), + Some(bid_id.to_string()), + ), + Err(err) => return Err(SaveTransferError::WrongConsigSwap(err)), + }, + }; + + let contract_id = transfer.contract_id().to_string(); + Ok(RgbExtractTransfer { + consig_id: transfer.id().to_string(), + contract_id, + tx_id, + transfer, + offer_id, + bid_id, + strict: confined, + }) +} diff --git a/src/rgb/prefetch.rs b/src/rgb/prefetch.rs index 194d538d..6c2b30b6 100644 --- a/src/rgb/prefetch.rs +++ b/src/rgb/prefetch.rs @@ -101,16 +101,14 @@ pub async fn prefetch_resolver_rgb( asset_type: Option, ) { use crate::rgb::import::{contract_from_armored, contract_from_other_formats}; + use crate::rgb::prebuild::prebuild_extract_transfer; use amplify::confinement::U32; use rgbstd::contract::Genesis; let esplora_client: EsploraBlockchain = EsploraBlockchain::new(&explorer.explorer_url, 1).with_concurrency(6); - let contract = if contract.starts_with("-----BEGIN RGB CONTRACT-----") { - contract_from_armored(contract) - } else { - contract_from_other_formats(contract, asset_type, None) - }; + let contract = prebuild_extract_transfer(contract).expect("invalid transfer"); + let contract = contract.transfer.unbindle(); for anchor_bundle in contract.bundles { let transaction_id = &bitcoin::Txid::from_str(&anchor_bundle.anchor.txid.to_hex()) diff --git a/src/rgb/psbt.rs b/src/rgb/psbt.rs index ad585cfd..39709cdd 100644 --- a/src/rgb/psbt.rs +++ b/src/rgb/psbt.rs @@ -87,6 +87,7 @@ pub fn create_psbt( psbt_inputs: Vec, psbt_outputs: Vec, bitcoin_fee: u64, + sighash: Option, terminal_change: Option, wallet: Option, tx_resolver: &impl ResolveTx, @@ -124,6 +125,7 @@ pub fn create_psbt( let new_input = InputDescriptor::resolve_psbt_input( psbt_input, global_descriptor.clone(), + sighash, wallet.clone(), tx_resolver, ) @@ -154,7 +156,7 @@ pub fn create_psbt( .map_err(CreatePsbtError::WrongTerminal)?; } - let mut psbt = Psbt::construct( + let psbt = Psbt::construct( global_descriptor, &inputs, &outputs, @@ -164,9 +166,14 @@ pub fn create_psbt( ) .map_err(|op| CreatePsbtError::Incomplete(op.to_string()))?; + Ok((psbt, change_index.to_string())) +} + +pub fn set_tapret_position(psbt: Psbt, pos: u16) -> Result { // Define Tapret Proprierties + let mut psbt = psbt; let proprietary_keys = vec![ProprietaryKeyDescriptor { - location: ProprietaryKeyLocation::Output((psbt.outputs.len() - 1) as u16), + location: ProprietaryKeyLocation::Output(pos), ty: ProprietaryKeyType { prefix: RGB_PSBT_TAPRET.to_owned(), subtype: 0, @@ -208,7 +215,7 @@ pub fn create_psbt( } } - Ok((psbt, change_index.to_string())) + Ok(psbt) } pub fn extract_commit(psbt: Psbt) -> Result<(Outpoint, Vec), DbcPsbtError> { @@ -379,6 +386,7 @@ where let new_input = InputDescriptor::resolve_psbt_input( psbt_input, global_descriptor.clone(), + None, Some(wallet.clone()), resolver, ) @@ -439,6 +447,7 @@ pub trait PsbtInputEx { fn resolve_psbt_input( psbt_input: PsbtInputRequest, descriptor: Descriptor, + sighash: Option, wallet: Option, tx_resolver: &impl ResolveTx, ) -> Result; @@ -450,10 +459,17 @@ impl PsbtInputEx for InputDescriptor { fn resolve_psbt_input( psbt_input: PsbtInputRequest, descriptor: Descriptor, + sighash: Option, wallet: Option, tx_resolver: &impl ResolveTx, ) -> Result { let outpoint: OutPoint = psbt_input.utxo.parse().expect("invalid outpoint parse"); + + let sighash = match sighash { + Some(ty) => ty, + None => EcdsaSighashType::All, + }; + let mut input: InputDescriptor = InputDescriptor { outpoint, terminal: psbt_input @@ -462,7 +478,7 @@ impl PsbtInputEx for InputDescriptor { .map_err(|_| PsbtInputError::WrongTerminal)?, seq_no: SeqNo::default(), tweak: None, - sighash_type: EcdsaSighashType::All, + sighash_type: sighash, }; // Verify TapTweak (User Input or Watcher inspect) diff --git a/src/rgb/structs.rs b/src/rgb/structs.rs index 33b37a43..a8b0e4e7 100644 --- a/src/rgb/structs.rs +++ b/src/rgb/structs.rs @@ -3,11 +3,13 @@ use std::{ str::FromStr, }; +use amplify::confinement::{Confined, U32}; use bitcoin::Address; use bitcoin_scripts::address::AddressCompat; use bp::Txid; use rgb::{RgbWallet, TerminalPath}; +use rgbstd::containers::{Bindle, Transfer}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Hash, Debug, Display)] @@ -62,3 +64,14 @@ pub struct RgbTransfer { pub tx: Txid, pub is_send: bool, } + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct RgbExtractTransfer { + pub consig_id: String, + pub contract_id: String, + pub tx_id: Txid, + pub transfer: Bindle, + pub strict: Confined, 0, U32>, + pub offer_id: Option, + pub bid_id: Option, +} diff --git a/src/rgb/swap.rs b/src/rgb/swap.rs new file mode 100644 index 00000000..b2256309 --- /dev/null +++ b/src/rgb/swap.rs @@ -0,0 +1,890 @@ +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}, + Array, Bytes32, RawArray, +}; +use autosurgeon::{reconcile, Hydrate, Reconcile}; +use baid58::{Baid58ParseError, FromBaid58, ToBaid58}; +use bitcoin::psbt::{PartiallySignedTransaction, Psbt}; +use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, Secp256k1, SecretKey}; +use bitcoin_scripts::address::AddressCompat; +use bp::Txid; +use core::fmt::Display; +use garde::Validate; +use rgbstd::{ + containers::{Bindle, Transfer}, + validation::{AnchoredBundle, ConsignmentApi}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + cmp, + collections::{btree_map, BTreeMap}, + fmt::{self, Formatter}, + str::FromStr, +}; +use strict_encoding::{ + StrictDecode, StrictDeserialize, StrictDumb, StrictEncode, StrictSerialize, StrictType, +}; + +type AssetId = String; +type OfferId = String; +type BidId = String; +type TransferId = String; + +#[derive( + Eq, + Ord, + PartialEq, + PartialOrd, + Serialize, + Deserialize, + Reconcile, + Hydrate, + Clone, + Debug, + Display, + Default, +)] +pub enum RgbOrderStatus { + #[default] + #[display(inner)] + #[serde(rename = "open")] + Open, + #[serde(rename = "fill")] + Fill, +} + +#[derive(Clone, Serialize, Deserialize, Validate, Reconcile, Hydrate, Debug, Display, Default)] +#[garde(context(RGBContext))] +#[display("{offer_id} / {contract_id}:{asset_amount} / {bitcoin_price}")] +pub struct RgbOffer { + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: OfferId, + #[garde(skip)] + pub offer_status: RgbOrderStatus, + #[garde(ascii)] + pub contract_id: AssetId, + #[garde(ascii)] + pub iface: String, + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub asset_amount: u64, + #[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, + #[garde(ascii)] + pub public: String, + #[garde(skip)] + pub transfer_id: Option, +} + +impl RgbOffer { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + secret: String, + contract_id: String, + iface: String, + allocations: Vec, + seller_address: AddressCompat, + bitcoin_price: u64, + psbt: String, + expire_at: Option, + ) -> Self { + let secp = Secp256k1::new(); + let secret = hex::decode(secret).expect("cannot decode hex sk in new RgbOffer"); + let secret_key = SecretKey::from_slice(&secret).expect("error parsing sk in new RgbOffer"); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + let asset_amount = allocations + .clone() + .into_iter() + .map(|a| match a.value { + crate::structs::AllocationValue::Value(amount) => amount, + crate::structs::AllocationValue::UDA(_) => 1, + }) + .sum(); + + let mut asset_utxos: Vec = allocations.into_iter().map(|a| a.utxo).collect(); + asset_utxos.sort(); + + let mut hasher = blake3::Hasher::new(); + for asset_utxo in asset_utxos { + hasher.update(asset_utxo.as_bytes()); + } + + let id = Array::from_array(hasher.finalize().into()); + let order_id = OrderId(id); + let order_id = order_id.to_baid58_string(); + + RgbOffer { + offer_id: order_id.to_string(), + offer_status: RgbOrderStatus::Open, + contract_id, + iface, + asset_amount, + bitcoin_price, + seller_psbt: psbt, + seller_address: seller_address.to_string(), + public: public_key.to_hex(), + expire_at, + ..Default::default() + } + } +} + +#[derive(Clone, Serialize, Deserialize, Validate, Reconcile, Hydrate, Debug, Display, Default)] +#[garde(context(RGBContext))] +#[display("{offer_id} / {contract_id}:{asset_amount} / {bitcoin_price}")] +pub struct RgbOfferSwap { + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: OfferId, + #[garde(ascii)] + pub contract_id: AssetId, + #[garde(ascii)] + pub iface: String, + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub asset_amount: u64, + #[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, + #[garde(ascii)] + pub public: String, +} + +impl From for RgbOfferSwap { + fn from(value: RgbOffer) -> Self { + let RgbOffer { + offer_id, + contract_id, + iface, + asset_amount, + bitcoin_price, + seller_psbt, + seller_address, + public, + expire_at, + .. + } = value; + + Self { + offer_id, + contract_id, + iface, + asset_amount, + bitcoin_price, + seller_psbt, + seller_address, + public, + expire_at, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Validate, Reconcile, Hydrate, Debug, Default, Display)] +#[garde(context(RGBContext))] +#[display("{bid_id} / {contract_id}:{asset_amount} / {bitcoin_amount}")] +pub struct RgbBid { + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub bid_id: BidId, + #[garde(skip)] + pub bid_status: RgbOrderStatus, + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: OfferId, + #[garde(skip)] + pub contract_id: AssetId, + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub asset_amount: u64, + #[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 transfer_id: Option, +} + +impl RgbBid { + pub(crate) fn new( + secret: String, + offer_id: OfferId, + contract_id: AssetId, + asset_amount: u64, + bitcoin_price: u64, + bitcoin_utxos: Vec, + ) -> Self { + 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 mut allocations = bitcoin_utxos; + allocations.sort(); + + let mut hasher = blake3::Hasher::new(); + for allocation in allocations { + hasher.update(allocation.as_bytes()); + } + + let id = Array::from_array(hasher.finalize().into()); + let order_id = OrderId(id); + let order_id = order_id.to_baid58_string(); + + RgbBid { + bid_id: order_id.to_string(), + bid_status: RgbOrderStatus::Open, + offer_id, + contract_id, + asset_amount, + bitcoin_amount: bitcoin_price, + public: public_key.to_hex(), + ..Default::default() + } + } +} + +#[derive(Clone, Serialize, Deserialize, Validate, Reconcile, Hydrate, Debug, Default, Display)] +#[garde(context(RGBContext))] +#[display("{bid_id} / {contract_id}:{asset_amount} / {bitcoin_amount}")] +pub struct RgbBidSwap { + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub bid_id: BidId, + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: OfferId, + #[garde(skip)] + pub contract_id: AssetId, + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub asset_amount: u64, + #[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, +} + +impl From for RgbBidSwap { + fn from(value: RgbBid) -> Self { + let RgbBid { + bid_id, + offer_id, + contract_id, + asset_amount, + bitcoin_amount, + buyer_psbt, + buyer_invoice, + public, + .. + } = value; + + Self { + bid_id, + offer_id, + contract_id, + asset_amount, + bitcoin_amount, + buyer_psbt, + buyer_invoice, + public, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Validate, Reconcile, Hydrate, Debug, Default, Display)] +#[garde(context(RGBContext))] +#[display("{bid_id}:{asset_amount} = {bitcoin_amount}")] +pub struct PublicRgbBid { + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub bid_id: BidId, + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub asset_amount: u64, + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub bitcoin_amount: u64, + #[garde(ascii)] + pub public: String, +} + +impl From for PublicRgbBid { + fn from(value: RgbBidSwap) -> Self { + let RgbBidSwap { + bid_id, + asset_amount, + bitcoin_amount, + public, + .. + } = value; + + Self { + bid_id, + asset_amount, + bitcoin_amount, + public, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] +pub struct RgbOffers { + pub offers: BTreeMap>, + pub bids: BTreeMap>, +} + +#[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] +pub struct RgbBids { + pub bids: BTreeMap>, +} + +#[derive(Clone, Serialize, Deserialize, Reconcile, Hydrate, Default, Debug)] +pub struct PublicRgbOffers { + pub offers: BTreeMap>, + pub bids: BTreeMap>, +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, From, Error)] +#[display(doc_comments)] +pub enum RgbOfferErrors { + /// Occurs an error in retrieve offers. {0} + IO(RgbPersistenceError), + /// Occurs an error in retrieve keys. {0} + Keys(String), + /// Offer #{0} is not found in public orderbook. + NoOffer(String), + /// Bid #{0} is not found in public orderbook. + NoBid(String), + /// Occurs an error in merge step. {0} + AutoMerge(String), +} + +pub async fn get_public_offer(offer_id: OfferId) -> Result { + let LocalRgbOffers { doc: _, 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); + } + + let offer = match public_offers.into_iter().find(|x| x.offer_id == offer_id) { + Some(offer) => offer.clone(), + _ => return Err(RgbOfferErrors::NoOffer(offer_id)), + }; + + Ok(offer) +} + +pub async fn get_public_bid( + offer_id: OfferId, + bid_id: BidId, +) -> Result { + let LocalRgbOffers { doc: _, rgb_offers } = + retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; + + let public_bids = match rgb_offers.bids.get(&offer_id) { + Some(bids) => bids, + _ => return Err(RgbOfferErrors::NoOffer(offer_id)), + }; + + let public_bid = match public_bids.get(&bid_id) { + Some(bid) => bid.clone(), + _ => return Err(RgbOfferErrors::NoBid(bid_id)), + }; + + Ok(public_bid) +} + +pub async fn get_swap_bid( + sk: &str, + offer_id: String, + bid_id: BidId, + expire_at: Option, +) -> Result { + let bid = get_public_bid(offer_id.clone(), bid_id.clone()).await?; + + 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()))?; + let public_key = + PublicKey::from_str(&bid.public).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(); + + let file_name = format!("{offer_id}-{bid_id}"); + let LocalRgbOfferBid { rgb_bid, .. } = + retrieve_swap_offer_bid(&share_sk, &file_name, expire_at) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(rgb_bid) +} + +pub async fn publish_public_offer(new_offer: RgbOfferSwap) -> Result<(), RgbOfferErrors> { + let LocalRgbOffers { + doc, + mut rgb_offers, + } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; + + let mut local_copy = automerge::AutoCommit::load(&doc) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + if let Some(offers) = rgb_offers.offers.get(&new_offer.contract_id) { + let mut avaliable_offers = offers.to_owned(); + avaliable_offers.push(new_offer.clone()); + rgb_offers + .offers + .insert(new_offer.clone().contract_id, avaliable_offers); + } else { + rgb_offers + .offers + .insert(new_offer.clone().contract_id, vec![new_offer]); + } + + // TODO: Add change verification (accept only addition operation) + reconcile(&mut local_copy, rgb_offers) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_public_offers(local_copy.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(()) +} + +pub async fn publish_public_bid(new_bid: RgbBidSwap) -> Result<(), RgbOfferErrors> { + let RgbBidSwap { + bid_id, offer_id, .. + } = new_bid.clone(); + + let _ = get_public_offer(offer_id.clone()).await?; + let LocalRgbOffers { + doc, + mut rgb_offers, + } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; + + let mut local_copy = automerge::AutoCommit::load(&doc) + .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 avaliable_bids = bids.to_owned(); + avaliable_bids.insert(bid_id, new_public_bid); + rgb_offers.bids.insert(offer_id.clone(), avaliable_bids); + } else { + rgb_offers + .bids + .insert(offer_id.clone(), bmap! { bid_id => new_public_bid }); + } + + // TODO: Add change verification (accept only addition operation) + reconcile(&mut local_copy, rgb_offers) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_public_offers(local_copy.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(()) +} + +pub async fn publish_swap_bid( + sk: &str, + offer_pub: &str, + new_bid: RgbBidSwap, + expire_at: Option, +) -> Result<(), RgbOfferErrors> { + let RgbBidSwap { + bid_id, offer_id, .. + } = new_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()))?; + let public_key = + PublicKey::from_str(offer_pub).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(); + 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) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + reconcile(&mut local_copy, new_bid).map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_swap_bids(&share_sk, &file_name, local_copy.save(), expire_at) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(()) +} + +pub async fn remove_public_offers(offers: Vec) -> Result<(), RgbOfferErrors> { + let LocalRgbOffers { + doc, + mut rgb_offers, + } = retrieve_public_offers().await.map_err(RgbOfferErrors::IO)?; + + let mut local_copy = automerge::AutoCommit::load(&doc) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + let current_public_offers = rgb_offers.clone(); + for offer in offers { + if let Some(public_offers) = current_public_offers.offers.get(&offer.contract_id) { + let public_offers = public_offers.to_owned(); + if public_offers.iter().any(|x| x.offer_id == offer.offer_id) { + let others = public_offers + .iter() + .filter(|x| x.offer_id != offer.offer_id) + .map(|x| x.to_owned()) + .collect(); + rgb_offers.offers.insert(offer.contract_id, others); + rgb_offers.bids.remove(&offer.offer_id); + } + } + } + + // TODO: Add change verification (accept only addition operation) + reconcile(&mut local_copy, rgb_offers) + .map_err(|op| RgbOfferErrors::AutoMerge(op.to_string()))?; + + store_public_offers(local_copy.save()) + .await + .map_err(RgbOfferErrors::IO)?; + + Ok(()) +} + +pub async fn mark_transfer_offer( + offer_id: OfferId, + consig_id: TransferId, + rgb_offers: &mut RgbOffers, +) -> Result<(), RgbOfferErrors> { + let offers = rgb_offers.offers.clone(); + for (contract_id, mut my_offers) in offers { + if let Some(position) = my_offers.iter().position(|x| x.offer_id == offer_id) { + let mut offer = my_offers.swap_remove(position); + offer.transfer_id = Some(consig_id.to_owned()); + + my_offers.insert(position, offer); + rgb_offers.offers.insert(contract_id, my_offers); + break; + } + } + Ok(()) +} + +pub async fn mark_transfer_bid( + bid_id: BidId, + consig_id: TransferId, + rgb_bids: &mut RgbBids, +) -> Result<(), RgbOfferErrors> { + let bids = rgb_bids.bids.clone(); + for (contract_id, mut my_bids) in bids { + 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()); + + my_bids.insert(position, offer); + rgb_bids.bids.insert(contract_id, my_bids); + break; + } + } + Ok(()) +} + +pub async fn mark_offer_fill( + transfer_id: TransferId, + rgb_offers: &mut RgbOffers, +) -> Result, RgbOfferErrors> { + let mut offer_filled = None; + let offers = rgb_offers.offers.clone(); + for (contract_id, mut my_offers) in offers { + if let Some(position) = my_offers + .clone() + .into_iter() + .position(|x| x.transfer_id.unwrap_or_default() == transfer_id) + { + let mut offer = my_offers.swap_remove(position); + offer.offer_status = RgbOrderStatus::Fill; + + offer_filled = Some(offer.clone()); + my_offers.insert(position, offer); + rgb_offers.offers.insert(contract_id, my_offers); + + break; + } + } + Ok(offer_filled) +} + +pub async fn mark_bid_fill( + transfer_id: TransferId, + rgb_bids: &mut RgbBids, +) -> Result, RgbOfferErrors> { + let mut bid_filled = None; + let bids = rgb_bids.bids.clone(); + for (contract_id, mut my_bids) in bids { + if let Some(position) = my_bids + .clone() + .into_iter() + .position(|x| x.transfer_id.unwrap_or_default() == transfer_id) + { + let mut bid = my_bids.swap_remove(position); + bid.bid_status = RgbOrderStatus::Fill; + + bid_filled = Some(bid.clone()); + my_bids.insert(position, bid); + rgb_bids.bids.insert(contract_id, my_bids); + + break; + } + } + Ok(bid_filled) +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum PsbtSwapExError { + /// The Input PSBT is invalid (Unexpected behavior). + Inconclusive, +} + +pub trait PsbtSwapEx { + type Error: std::error::Error; + + /// Join this [`PartiallySignedTransaction`] with `other` PSBT as described by BIP 174. + /// + /// The join method emulate the same behavior of the rpc method `joinpsbts` + /// See: https://developer.bitcoin.org/reference/rpc/joinpsbts.html + fn join(self, other: T) -> Result; +} + +impl PsbtSwapEx for PartiallySignedTransaction { + type Error = PsbtSwapExError; + + fn join( + self, + other: PartiallySignedTransaction, + ) -> 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(); + new_psbt.version = cmp::max(new_psbt.version, other.version); + + // Merging xpubs + for (xpub, (fingerprint1, derivation1)) in other.xpub { + match new_psbt.xpub.entry(xpub) { + btree_map::Entry::Vacant(entry) => { + entry.insert((fingerprint1, derivation1)); + } + btree_map::Entry::Occupied(mut entry) => { + // Here in case of the conflict we select the version with algorithm: + // 1) if everything is equal we do nothing + // 2) report an error if + // - derivation paths are equal and fingerprints are not + // - derivation paths are of the same length, but not equal + // - derivation paths has different length, but the shorter one + // is not the strict suffix of the longer one + // 3) choose longest derivation otherwise + + let (fingerprint2, derivation2) = entry.get().clone(); + + if (derivation1 == derivation2 && fingerprint1 == fingerprint2) + || (derivation1.len() < derivation2.len() + && derivation1[..] + == derivation2[derivation2.len() - derivation1.len()..]) + { + continue; + } else if derivation2[..] + == derivation1[derivation1.len() - derivation2.len()..] + { + entry.insert((fingerprint1, derivation1)); + continue; + } + return Err(PsbtSwapExError::Inconclusive); + } + } + } + + new_psbt.proprietary.extend(other.proprietary); + new_psbt.unknown.extend(other.unknown); + + new_psbt.inputs.extend(other.inputs); + new_psbt.outputs.extend(other.outputs); + + // Transaction + new_psbt.unsigned_tx.version = + cmp::max(new_psbt.unsigned_tx.version, other.unsigned_tx.version); + + new_psbt.unsigned_tx.lock_time = + cmp::max(new_psbt.unsigned_tx.lock_time, other.unsigned_tx.lock_time); + + new_psbt.unsigned_tx.input.extend(other.unsigned_tx.input); + new_psbt.unsigned_tx.output.extend(other.unsigned_tx.output); + + Ok(new_psbt.clone()) + } +} + +/// Swap Order identifier. +/// +/// Interface identifier commits to all of the interface data. +#[derive( + Wrapper, + Copy, + Clone, + Ord, + PartialOrd, + Eq, + PartialEq, + Hash, + Debug, + From, + StrictType, + StrictDumb, + StrictEncode, + StrictDecode, +)] +#[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)] +#[strict_type(lib = LIB_NAME_BITMASK)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(crate = "serde_crate", transparent) +)] +pub struct OrderId( + #[from] + #[from([u8; 32])] + Bytes32, +); + +impl ToBaid58<32> for OrderId { + const HRI: &'static str = "swap"; + fn to_baid58_payload(&self) -> [u8; 32] { + self.to_raw_array() + } +} +impl FromBaid58<32> for OrderId {} +impl Display for OrderId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if f.sign_minus() { + write!(f, "urn:diba:{::<}", self.to_baid58()) + } else { + write!(f, "urn:diba:{::<#}", self.to_baid58()) + } + } +} +impl FromStr for OrderId { + type Err = Baid58ParseError; + fn from_str(s: &str) -> Result { + Self::from_baid58_str(s.trim_start_matches("urn:diba:")) + } +} + +#[derive(Clone, Debug, StrictType, StrictDumb, StrictEncode, StrictDecode)] +#[strict_type(lib = LIB_NAME_BITMASK)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(crate = "serde_crate", rename_all = "camelCase") +)] +pub struct TransferSwap { + pub offer_id: OrderId, + pub bid_id: OrderId, + pub consig: Transfer, +} + +impl StrictSerialize for TransferSwap {} +impl StrictDeserialize for TransferSwap {} + +impl TransferSwap { + pub fn with(offer_id: &str, bid_id: &str, transfer: Transfer) -> Self { + let offer_id = OrderId::from_str(offer_id).expect("Invalid rgb offer Id"); + let bid_id = OrderId::from_str(bid_id).expect("Invalid rgb bid Id"); + + Self { + offer_id, + bid_id, + consig: transfer, + } + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +// TODO: Complete errors +pub enum TransferSwapError { + /// Consignment data have an invalid hexadecimal format. + WrongHex, + /// ContractID cannot be decoded. {0} + WrongContract(String), + /// Consignment cannot be decoded. {0} + WrongConsig(String), + /// Network cannot be decoded. {0} + WrongNetwork(String), + /// The Consignment is invalid. Details: {0:?} + InvalidConsig(Vec), + /// The Consignment is invalid (Unexpected behavior on validation). + Inconclusive, +} + +pub fn extract_transfer( + transfer: String, +) -> Result<(Txid, Bindle, OrderId, OrderId), TransferSwapError> { + let serialized = Vec::::from_hex(&transfer).map_err(|_| TransferSwapError::WrongHex)?; + let confined = Confined::try_from_iter(serialized.iter().copied()) + .map_err(|err| TransferSwapError::WrongConsig(err.to_string()))?; + + let transfer_swap = TransferSwap::from_strict_serialized::<{ U32 }>(confined) + .map_err(|err| TransferSwapError::WrongConsig(err.to_string()))?; + + let transfer = transfer_swap.consig; + for (bundle_id, _) in transfer.terminals() { + if transfer.known_transitions_by_bundle_id(bundle_id).is_none() { + return Err(TransferSwapError::Inconclusive); + }; + if let Some(AnchoredBundle { anchor, bundle: _ }) = transfer.anchored_bundle(bundle_id) { + return Ok(( + anchor.txid, + Bindle::new(transfer), + transfer_swap.offer_id, + transfer_swap.bid_id, + )); + } + } + + Err(TransferSwapError::Inconclusive) +} diff --git a/src/rgb/transfer.rs b/src/rgb/transfer.rs index 1a3d5152..494de391 100644 --- a/src/rgb/transfer.rs +++ b/src/rgb/transfer.rs @@ -21,6 +21,8 @@ use rgbwallet::{InventoryWallet, InvoiceParseError, RgbInvoice, RgbTransport}; use seals::txout::ExplicitSeal; use strict_encoding::{StrictDeserialize, TypeName}; +use crate::rgb::prebuild::prebuild_extract_transfer; + #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] #[display(doc_comments)] pub enum NewInvoiceError { @@ -191,12 +193,10 @@ where T: ResolveHeight + ResolveTx, T::Error: 'static, { - let serialized = Vec::::from_hex(&transfer).map_err(|_| AcceptTransferError::WrongHex)?; - let confined = Confined::try_from_iter(serialized.iter().copied()) - .map_err(|err| AcceptTransferError::WrongConsig(err.to_string()))?; - let transfer = Transfer::from_strict_serialized::<{ U32 }>(confined) - .map_err(|err| AcceptTransferError::WrongConsig(err.to_string()))?; + let transfer_extracted = prebuild_extract_transfer(&transfer) + .map_err(|op| AcceptTransferError::WrongConsig(op.to_string()))?; + let transfer = transfer_extracted.transfer.unbindle(); let consig = transfer.validate(resolver).map_err(|err| { if let Some(status) = err.into_validation_status() { let mut messages = vec![]; @@ -219,6 +219,7 @@ pub fn extract_transfer(transfer: String) -> Result<(Txid, Bindle), Ac let serialized = Vec::::from_hex(&transfer).map_err(|_| AcceptTransferError::WrongHex)?; let confined = Confined::try_from_iter(serialized.iter().copied()) .map_err(|err| AcceptTransferError::WrongConsig(err.to_string()))?; + let transfer = Transfer::from_strict_serialized::<{ U32 }>(confined) .map_err(|err| AcceptTransferError::WrongConsig(err.to_string()))?; diff --git a/src/structs.rs b/src/structs.rs index 850265d6..94fecc5d 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -9,9 +9,12 @@ pub use bdk::{Balance, BlockTime, TransactionDetails}; pub use bitcoin::{util::address::Address, Txid}; use rgbstd::interface::rgb21::Allocation as AllocationUDA; -use crate::validators::{ - verify_descriptor, verify_media_types, verify_rgb_invoice, verify_tapret_seal, - verify_terminal_path, RGBContext, +use crate::{ + rgb::swap::{PublicRgbBid, RgbBid, RgbOffer, RgbOfferSwap}, + validators::{ + verify_descriptor, verify_media_types, verify_rgb_invoice, verify_tapret_seal, + verify_terminal_path, RGBContext, + }, }; #[derive(Serialize, Deserialize)] @@ -313,7 +316,7 @@ pub struct ReIssueResponse { pub contracts: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Eq, PartialEq, Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub enum AssetType { #[serde(rename = "bitcoin")] @@ -565,13 +568,29 @@ pub struct SignPsbtRequest { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct SignPsbtResponse { +pub struct SignedPsbtResponse { + /// PSBT is signed? + pub sign: bool, + /// PSBT signed + pub psbt: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PublishedPsbtResponse { /// PSBT is signed? pub sign: bool, /// Transaction id pub txid: String, } +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct RgbTransferInternalParams { + pub offer_id: Option, + pub bid_id: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[derive(Validate)] @@ -1023,6 +1042,7 @@ pub struct BatchRgbTransferItem { pub iface: String, pub status: TxStatus, pub is_accept: bool, + pub is_mine: bool, } #[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, Debug, Display)] @@ -1033,3 +1053,275 @@ pub struct UtxoSpentStatus { pub block_height: TxStatus, pub spent_height: TxStatus, } + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] +#[garde(context(RGBContext))] +#[serde(rename_all = "camelCase")] +#[display("{contract_id}:{contract_amount} ** {change_terminal}")] +pub struct RgbOfferRequest { + /// The Contract ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub contract_id: String, + /// The Contract Interface + #[garde(ascii)] + #[garde(length(min = 0, max = 32))] + pub iface: String, + /// Contract Amount + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub contract_amount: u64, + /// Bitcoin Price (in sats) + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub bitcoin_price: u64, + /// Universal Descriptor + #[garde(custom(verify_descriptor))] + pub descriptor: SecretString, + /// Asset Terminal Change + #[garde(ascii)] + pub change_terminal: String, + /// Bitcoin Change Addresses (format: {address}:{amount}) + #[garde(length(min = 0, max = 999))] + pub bitcoin_changes: Vec, + #[garde(skip)] + pub expire_at: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{offer_id}:{contract_amount} = {bitcoin_price}")] +pub struct RgbOfferResponse { + /// The Contract ID + pub offer_id: String, + /// The Contract ID + pub contract_id: String, + /// Contract Amount + pub contract_amount: u64, + /// Bitcoin Price + pub bitcoin_price: u64, + /// Seller Address + pub seller_address: String, + /// Seller PSBT (encoded in base64) + pub seller_psbt: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] +#[garde(context(RGBContext))] +#[serde(rename_all = "camelCase")] +#[display("{offer_id}:{asset_amount} ** {change_terminal}")] +pub struct RgbBidRequest { + /// The Offer ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: String, + /// Asset Amount + #[garde(range(min = u64::MIN, max = u64::MAX))] + pub asset_amount: u64, + /// Universal Descriptor + #[garde(custom(verify_descriptor))] + pub descriptor: SecretString, + /// Bitcoin Terminal Change + #[garde(ascii)] + pub change_terminal: String, + /// 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 RgbBidResponse { + /// The Bid ID + pub bid_id: String, + /// The Offer ID + pub offer_id: String, + /// Buyer Invoice + pub invoice: String, + /// Final PSBT (encoded in base64) + pub swap_psbt: String, + /// Fee Value + pub fee_value: u64, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default, Validate)] +#[garde(context(RGBContext))] +#[serde(rename_all = "camelCase")] +#[display("{offer_id} ~ {bid_id}")] +pub struct RgbSwapRequest { + /// Offer ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub offer_id: String, + /// Bid ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub bid_id: String, + /// Swap PSBT + #[garde(ascii)] + pub swap_psbt: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{consig_id}")] +pub struct RgbSwapResponse { + /// Transfer ID + pub consig_id: String, + /// Final Consig + pub final_consig: String, + /// Final PSBT + pub final_psbt: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{offers:?}")] +pub struct PublicRgbOffersResponse { + /// Public Offers + pub offers: Vec, + + /// Public Bids + pub bids: BTreeMap>, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{offer_id} ~ {contract_id}:{asset_amount} = {bitcoin_price}")] +pub struct PublicRgbOfferResponse { + /// Offer ID + offer_id: String, + /// Contract ID + contract_id: String, + /// Offer PubKey + offer_pub: String, + /// Asset/Contract Amount + asset_amount: u64, + /// Bitcoin Price + bitcoin_price: u64, + /// Initial Offer PSBT + offer_psbt: String, +} + +impl From for PublicRgbOfferResponse { + fn from(value: RgbOfferSwap) -> Self { + Self { + contract_id: value.contract_id, + offer_id: value.offer_id, + asset_amount: value.asset_amount, + bitcoin_price: value.bitcoin_price, + offer_pub: value.public, + offer_psbt: value.seller_psbt, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{bid_id}:{asset_amount} = {bitcoin_price}")] +pub struct PublicRgbBidResponse { + /// Bid ID + bid_id: String, + /// Asset/Contract Amount + asset_amount: u64, + /// Bitcoin Price + bitcoin_price: u64, +} + +impl From for PublicRgbBidResponse { + fn from(value: PublicRgbBid) -> Self { + let PublicRgbBid { + bid_id, + asset_amount, + bitcoin_amount, + .. + } = value; + + Self { + bid_id, + asset_amount, + bitcoin_price: bitcoin_amount, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct RgbOfferBidsResponse { + /// Offers + pub offers: Vec, + /// bids + pub bids: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct RgbOffersResponse { + /// Offers + pub offers: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct RgbBidsResponse { + /// Bids + pub bids: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{offer_id} ~ {contract_id}:{asset_amount} = {bitcoin_price}")] +pub struct RgbOfferDetail { + /// Contract ID + contract_id: String, + /// Offer ID + offer_id: String, + /// Offer Status + offer_status: String, + /// Asset/Contract Amount + asset_amount: u64, + /// Bitcoin Price + bitcoin_price: u64, +} + +impl From for RgbOfferDetail { + fn from(value: RgbOffer) -> Self { + Self { + contract_id: value.contract_id, + offer_id: value.offer_id, + offer_status: value.offer_status.to_string(), + asset_amount: value.asset_amount, + bitcoin_price: value.bitcoin_price, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, Display, Default)] +#[serde(rename_all = "camelCase")] +#[display("{bid_id} ~ {contract_id}:{asset_amount} = {bitcoin_price}")] +pub struct RgbBidDetail { + /// Contract ID + contract_id: String, + /// Bid ID + bid_id: String, + /// Offer ID + offer_id: String, + /// Bid Status + bid_status: String, + /// Asset/Contract Amount + asset_amount: u64, + /// Bitcoin Price (in satoshis) + bitcoin_price: u64, +} + +impl From for RgbBidDetail { + fn from(value: RgbBid) -> Self { + Self { + contract_id: value.contract_id, + offer_id: value.offer_id, + bid_id: value.bid_id, + bid_status: value.bid_status.to_string(), + asset_amount: value.asset_amount, + bitcoin_price: value.bitcoin_amount, + } + } +} diff --git a/src/validators.rs b/src/validators.rs index aea69a49..6e72e5e0 100644 --- a/src/validators.rs +++ b/src/validators.rs @@ -13,7 +13,7 @@ use crate::structs::{IssueMetaRequest, IssueMetadata, SecretString}; #[display(inner)] pub enum RGBParamsError { /// wrong or unspecified seal closed method. Only TapRet (tapret1st) - /// is + /// is supported #[display(doc_comments)] NoClosedMethod, diff --git a/src/web.rs b/src/web.rs index 511e3257..22bc4720 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,7 +1,8 @@ use crate::structs::{ AcceptRequest, FullRgbTransferRequest, ImportRequest, InvoiceRequest, IssueRequest, - PsbtRequest, ReIssueRequest, RgbRemoveTransferRequest, RgbSaveTransferRequest, - RgbTransferRequest, SecretString, SignPsbtRequest, WatcherRequest, + PsbtRequest, ReIssueRequest, RgbBidRequest, RgbOfferRequest, RgbRemoveTransferRequest, + RgbSaveTransferRequest, RgbSwapRequest, RgbTransferRequest, SecretString, SignPsbtRequest, + WatcherRequest, }; // use crate::{carbonado, lightning, rgb}; @@ -453,6 +454,21 @@ pub mod rgb { }) } + #[wasm_bindgen] + pub fn psbt_sign_and_publish_file(_nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let psbt_req: SignPsbtRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::bitcoin::sign_and_publish_psbt_file(psbt_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 transfer_asset(nostr_hex_sk: String, request: JsValue) -> Promise { set_panic_hook(); @@ -752,6 +768,107 @@ pub mod rgb { } }) } + + #[wasm_bindgen] + pub fn create_offer(nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let offer_req: RgbOfferRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_seller_offer(&nostr_hex_sk, offer_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(); + + future_to_promise(async move { + let bid_req: RgbBidRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_buyer_bid(&nostr_hex_sk, bid_req).await { + 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_swap(nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let swap_req: RgbSwapRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::create_swap_transfer(&nostr_hex_sk, swap_req).await { + 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 public_offers(nostr_hex_sk: String) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::rgb::list_public_offers(&nostr_hex_sk).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 my_orders(nostr_hex_sk: String) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::rgb::list_my_orders(&nostr_hex_sk).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 my_offers(nostr_hex_sk: String) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::rgb::list_my_offers(&nostr_hex_sk).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 my_bids(nostr_hex_sk: String) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::rgb::list_my_bids(&nostr_hex_sk).await { + Ok(result) => Ok(JsValue::from_string( + serde_json::to_string(&result).unwrap(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } } pub mod lightning { diff --git a/tests/rgb.rs b/tests/rgb.rs index db91a967..da64b810 100644 --- a/tests/rgb.rs +++ b/tests/rgb.rs @@ -21,6 +21,7 @@ mod rgb { mod import; mod issue; mod states; + mod swaps; mod transfers; mod udas; pub mod utils; @@ -32,6 +33,7 @@ mod rgb { mod imports; mod stl_ids; mod stl_load; + mod swaps; mod transfers; mod utils; } diff --git a/tests/rgb/integration/accept.rs b/tests/rgb/integration/accept.rs index 3b02e791..f027fb79 100644 --- a/tests/rgb/integration/accept.rs +++ b/tests/rgb/integration/accept.rs @@ -1,7 +1,7 @@ #![cfg(not(target_arch = "wasm32"))] use anyhow::Result; use bitmask_core::{ - bitcoin::{save_mnemonic, sign_psbt_file}, + bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, rgb::{ create_watcher, list_transfers, remove_transfer, save_transfer, verify_transfers, watcher_next_address, @@ -97,7 +97,7 @@ pub async fn allow_save_read_remove_transfers() -> Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 5. Save Consig (Owner Side) @@ -258,7 +258,7 @@ pub async fn allow_save_and_accept_all_transfers() -> Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 5. Save Consig (Owner Side) diff --git a/tests/rgb/integration/collectibles.rs b/tests/rgb/integration/collectibles.rs index e67eb7ec..3846199c 100644 --- a/tests/rgb/integration/collectibles.rs +++ b/tests/rgb/integration/collectibles.rs @@ -4,7 +4,7 @@ // ISSUER_MNEMONIC, // }; // use bitmask_core::{ -// bitcoin::{save_mnemonic, sign_psbt_file}, +// bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, // rgb::accept_transfer, // structs::{AcceptRequest, SecretString, SignPsbtRequest}, // }; @@ -27,7 +27,7 @@ // psbt: transfer_resp.psbt, // descriptor: SecretString(issuer_keys.private.rgb_udas_descriptor_xprv), // }; -// let resp = sign_psbt_file(request).await; +// let resp = sign_and_publish_psbt_file(request).await; // assert!(resp.is_ok()); // let request = AcceptRequest { diff --git a/tests/rgb/integration/dustless.rs b/tests/rgb/integration/dustless.rs index 65fe7140..6ff32d80 100644 --- a/tests/rgb/integration/dustless.rs +++ b/tests/rgb/integration/dustless.rs @@ -5,7 +5,10 @@ use crate::rgb::integration::utils::{ }; use bdk::wallet::AddressIndex; use bitmask_core::{ - bitcoin::{fund_vault, get_new_address, get_wallet, new_mnemonic, sign_psbt_file, sync_wallet}, + bitcoin::{ + fund_vault, get_new_address, get_wallet, new_mnemonic, sign_and_publish_psbt_file, + sync_wallet, + }, rgb::{accept_transfer, create_watcher, full_transfer_asset, get_contract}, structs::{ AcceptRequest, FullRgbTransferRequest, PsbtFeeRequest, PsbtInputRequest, SecretString, @@ -97,7 +100,7 @@ async fn create_dustless_transfer_with_fee_value() -> anyhow::Result<()> { ] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; @@ -232,7 +235,7 @@ async fn create_dustless_transfer_with_fee_rate() -> anyhow::Result<()> { ] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; // println!("{:?}", resp); assert!(resp.is_ok()); diff --git a/tests/rgb/integration/fungibles.rs b/tests/rgb/integration/fungibles.rs index 22f7d62d..6f826c89 100644 --- a/tests/rgb/integration/fungibles.rs +++ b/tests/rgb/integration/fungibles.rs @@ -4,7 +4,7 @@ use crate::rgb::integration::utils::{ ISSUER_MNEMONIC, OWNER_MNEMONIC, }; use bitmask_core::{ - bitcoin::{save_mnemonic, sign_psbt_file}, + bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, rgb::accept_transfer, structs::{AcceptRequest, SecretString, SignPsbtRequest}, }; @@ -62,7 +62,7 @@ async fn allow_beneficiary_accept_transfer() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); let request = AcceptRequest { diff --git a/tests/rgb/integration/states.rs b/tests/rgb/integration/states.rs index 654b7204..c0519059 100644 --- a/tests/rgb/integration/states.rs +++ b/tests/rgb/integration/states.rs @@ -1,6 +1,6 @@ #![cfg(not(target_arch = "wasm32"))] use bitmask_core::{ - bitcoin::{save_mnemonic, sign_psbt_file}, + bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, rgb::{accept_transfer, create_watcher, get_contract}, structs::{AcceptRequest, DecryptedWalletData, SecretString, SignPsbtRequest, WatcherRequest}, }; @@ -89,7 +89,7 @@ async fn check_fungible_state_after_accept_consig() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -192,7 +192,7 @@ async fn check_uda_state_after_accept_consig() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; diff --git a/tests/rgb/integration/swaps.rs b/tests/rgb/integration/swaps.rs new file mode 100644 index 00000000..707ea3f8 --- /dev/null +++ b/tests/rgb/integration/swaps.rs @@ -0,0 +1,485 @@ +#![cfg(not(target_arch = "wasm32"))] +use crate::rgb::integration::utils::{ + get_uda_data, issuer_issue_contract_v2, send_some_coins, UtxoFilter, +}; +use bitmask_core::{ + bitcoin::{ + fund_vault, get_new_address, get_wallet, new_mnemonic, 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, verify_transfers, + }, + structs::{ + AcceptRequest, IssueResponse, PsbtFeeRequest, RgbBidRequest, RgbBidResponse, + RgbOfferRequest, RgbOfferResponse, RgbSwapRequest, RgbSwapResponse, SecretString, + SignPsbtRequest, SignedPsbtResponse, WatcherRequest, + }, +}; + +#[tokio::test] +async fn create_scriptless_swap() -> anyhow::Result<()> { + // 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 issuer_sk = &seller_keys.private.nostr_prv; + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: seller_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(issuer_sk, create_watch_req.clone()).await?; + + let owner_sk = &buyer_keys.private.nostr_prv; + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: buyer_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(owner_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 assets_address_2 = 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 uda_address_2 = 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, + &assets_address_2, + &uda_address_1, + &uda_address_2, + Some(1.1), + ) + .await?; + + // 3. Send some coins (Buyer) + let btc_address_1 = get_new_address( + &SecretString(buyer_keys.public.btc_descriptor_xpub.clone()), + None, + ) + .await?; + let asset_address_1 = get_new_address( + &SecretString(buyer_keys.public.rgb_assets_descriptor_xpub.clone()), + None, + ) + .await?; + + let default_coins = "0.1"; + send_some_coins(&btc_address_1, default_coins).await; + send_some_coins(&asset_address_1, default_coins).await; + + // 4. Issue Contract (Seller) + let issuer_resp = issuer_issue_contract_v2( + 1, + "RGB20", + 5, + false, + false, + None, + None, + Some(UtxoFilter::with_outpoint( + fund_vault.assets_output.unwrap_or_default(), + )), + Some(seller_keys.clone()), + ) + .await?; + let IssueResponse { + contract_id, + iface, + supply, + .. + } = issuer_resp[0].clone(); + + // 5. Create Seller Swap Side + let contract_amount = supply - 1; + let seller_sk = seller_keys.private.nostr_prv.clone(); + let bitcoin_price: u64 = 100000; + let seller_asset_desc = seller_keys.public.rgb_assets_descriptor_xpub.clone(); + let expire_at = (chrono::Local::now() + chrono::Duration::minutes(5)) + .naive_utc() + .timestamp(); + let seller_swap_req = RgbOfferRequest { + contract_id: contract_id.clone(), + iface, + contract_amount, + bitcoin_price, + descriptor: SecretString(seller_asset_desc), + change_terminal: "/20/1".to_string(), + bitcoin_changes: vec![], + expire_at: Some(expire_at), + }; + + let seller_swap_resp = create_seller_offer(&seller_sk, seller_swap_req).await; + assert!(seller_swap_resp.is_ok()); + + // 7. Create Buyer Swap Side + let RgbOfferResponse { + offer_id, + contract_amount, + .. + } = seller_swap_resp?; + + let buyer_sk = buyer_keys.private.nostr_prv.clone(); + let buyer_btc_desc = buyer_keys.public.btc_descriptor_xpub.clone(); + let buyer_swap_req = RgbBidRequest { + offer_id: offer_id.clone(), + asset_amount: contract_amount, + descriptor: SecretString(buyer_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(1000), + }; + + let buyer_swap_resp = create_buyer_bid(&buyer_sk, buyer_swap_req).await; + assert!(buyer_swap_resp.is_ok()); + + // 8. Sign the Buyer Side + let RgbBidResponse { + bid_id, swap_psbt, .. + } = buyer_swap_resp?; + let request = SignPsbtRequest { + psbt: swap_psbt, + descriptors: vec![ + SecretString(buyer_keys.private.btc_descriptor_xprv.clone()), + SecretString(buyer_keys.private.btc_change_descriptor_xprv.clone()), + ], + }; + let buyer_psbt_resp = sign_psbt_file(request).await; + assert!(buyer_psbt_resp.is_ok()); + + // 9. Create Swap PSBT + let SignedPsbtResponse { + psbt: swap_psbt, .. + } = buyer_psbt_resp?; + let final_swap_req = RgbSwapRequest { + offer_id, + bid_id, + swap_psbt, + }; + + let final_swap_resp = create_swap_transfer(issuer_sk, final_swap_req).await; + assert!(final_swap_resp.is_ok()); + + // 8. Sign the Final PSBT + let RgbSwapResponse { + final_consig, + final_psbt, + .. + } = final_swap_resp?; + + let request = SignPsbtRequest { + psbt: final_psbt.clone(), + descriptors: 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 seller_psbt_resp = sign_and_publish_psbt_file(request).await; + assert!(seller_psbt_resp.is_ok()); + + // 9. Accept Consig (Buyer/Seller) + let all_sks = [buyer_sk.clone(), seller_sk.clone()]; + for sk in all_sks { + let request = AcceptRequest { + consignment: final_consig.clone(), + force: false, + }; + let resp = accept_transfer(&sk, request).await; + assert!(resp.is_ok()); + assert!(resp?.valid); + } + + // 10 Mine Some Blocks + let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; + send_some_coins(whatever_address, "0.001").await; + + // 11. Retrieve Contract (Seller Side) + let resp = get_contract(&seller_sk, &contract_id).await; + assert!(resp.is_ok()); + assert_eq!(1, resp?.balance); + + // 12. Retrieve Contract (Buyer Side) + let resp = get_contract(&buyer_sk, &contract_id).await; + assert!(resp.is_ok()); + assert_eq!(4, resp?.balance); + + // 13. Verify transfers (Seller Side) + let resp = verify_transfers(&seller_sk).await; + assert!(resp.is_ok()); + assert_eq!(1, resp?.transfers.len()); + + Ok(()) +} + +#[tokio::test] +async fn create_scriptless_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?; + + let watcher_name = "default"; + let issuer_sk = &seller_keys.private.nostr_prv; + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: seller_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(issuer_sk, create_watch_req.clone()).await?; + + let owner_sk = &buyer_keys.private.nostr_prv; + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: buyer_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(owner_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 assets_address_2 = 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 uda_address_2 = 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, + &assets_address_2, + &uda_address_1, + &uda_address_2, + 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_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( + 1, + "RGB21", + 1, + false, + false, + Some(metadata), + None, + Some(UtxoFilter::with_outpoint( + fund_vault.udas_output.unwrap_or_default(), + )), + Some(seller_keys.clone()), + ) + .await?; + let IssueResponse { + contract_id, iface, .. + } = issuer_resp[0].clone(); + + // 5. Create Seller Swap Side + let contract_amount = 1; + let seller_sk = seller_keys.private.nostr_prv.clone(); + let bitcoin_price: u64 = 100000; + let seller_asset_desc = seller_keys.public.rgb_udas_descriptor_xpub.clone(); + let expire_at = (chrono::Local::now() + chrono::Duration::minutes(5)) + .naive_utc() + .timestamp(); + let seller_swap_req = RgbOfferRequest { + contract_id: contract_id.clone(), + iface, + contract_amount, + bitcoin_price, + descriptor: SecretString(seller_asset_desc), + change_terminal: "/21/1".to_string(), + bitcoin_changes: vec![], + expire_at: Some(expire_at), + }; + + let seller_swap_resp = create_seller_offer(&seller_sk, seller_swap_req).await; + assert!(seller_swap_resp.is_ok()); + + // 7. Create Buyer Swap Side + let RgbOfferResponse { + offer_id, + contract_amount, + .. + } = seller_swap_resp?; + + let buyer_sk = buyer_keys.private.nostr_prv.clone(); + let buyer_btc_desc = buyer_keys.public.btc_descriptor_xpub.clone(); + let buyer_swap_req = RgbBidRequest { + offer_id: offer_id.clone(), + asset_amount: contract_amount, + descriptor: SecretString(buyer_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(1000), + }; + + let buyer_swap_resp = create_buyer_bid(&buyer_sk, buyer_swap_req).await; + assert!(buyer_swap_resp.is_ok()); + + // 8. Sign the Buyer Side + let RgbBidResponse { + bid_id, swap_psbt, .. + } = buyer_swap_resp?; + let request = SignPsbtRequest { + psbt: swap_psbt, + descriptors: vec![ + SecretString(buyer_keys.private.btc_descriptor_xprv.clone()), + SecretString(buyer_keys.private.btc_change_descriptor_xprv.clone()), + ], + }; + let buyer_psbt_resp = sign_psbt_file(request).await; + assert!(buyer_psbt_resp.is_ok()); + + // 9. Create Swap PSBT + let SignedPsbtResponse { + psbt: swap_psbt, .. + } = buyer_psbt_resp?; + let final_swap_req = RgbSwapRequest { + offer_id, + bid_id, + swap_psbt, + }; + + let final_swap_resp = create_swap_transfer(issuer_sk, final_swap_req).await; + assert!(final_swap_resp.is_ok()); + + // 8. Sign the Final PSBT + let RgbSwapResponse { + final_consig, + final_psbt, + .. + } = final_swap_resp?; + + let request = SignPsbtRequest { + psbt: final_psbt.clone(), + descriptors: vec![ + SecretString(seller_keys.private.btc_descriptor_xprv.clone()), + SecretString(seller_keys.private.btc_change_descriptor_xprv.clone()), + SecretString(seller_keys.private.rgb_udas_descriptor_xprv.clone()), + ], + }; + let seller_psbt_resp = sign_and_publish_psbt_file(request).await; + assert!(seller_psbt_resp.is_ok()); + + // 9. Accept Consig (Buyer/Seller) + let all_sks = [buyer_sk.clone(), seller_sk.clone()]; + for sk in all_sks { + let request = AcceptRequest { + consignment: final_consig.clone(), + force: false, + }; + let resp = accept_transfer(&sk, request).await; + assert!(resp.is_ok()); + assert!(resp?.valid); + } + + // 10 Mine Some Blocks + let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; + send_some_coins(whatever_address, "0.001").await; + + // 11. Retrieve Contract (Seller Side) + let resp = get_contract(&seller_sk, &contract_id).await; + assert!(resp.is_ok()); + assert_eq!(0, resp?.balance); + + // 12. Retrieve Contract (Buyer Side) + let resp = get_contract(&buyer_sk, &contract_id).await; + assert!(resp.is_ok()); + assert_eq!(1, resp?.balance); + + // 13. Verify transfers (Seller Side) + let resp = verify_transfers(&seller_sk).await; + assert!(resp.is_ok()); + assert_eq!(1, resp?.transfers.len()); + + Ok(()) +} diff --git a/tests/rgb/integration/transfers.rs b/tests/rgb/integration/transfers.rs index 278b032b..21eb644b 100644 --- a/tests/rgb/integration/transfers.rs +++ b/tests/rgb/integration/transfers.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashMap}; use bdk::wallet::AddressIndex; use bitmask_core::{ - bitcoin::{get_wallet, new_mnemonic, save_mnemonic, sign_psbt_file, sync_wallet}, + bitcoin::{get_wallet, new_mnemonic, save_mnemonic, sign_and_publish_psbt_file, sync_wallet}, rgb::{ accept_transfer, create_invoice, create_watcher, full_transfer_asset, get_contract, import, save_transfer, verify_transfers, watcher_next_address, watcher_next_utxo, @@ -103,7 +103,7 @@ async fn allow_issuer_make_conseq_transfers() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -173,7 +173,7 @@ async fn allow_issuer_make_conseq_transfers() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -258,7 +258,7 @@ async fn allow_owner_make_conseq_transfers() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -336,7 +336,7 @@ async fn allow_owner_make_conseq_transfers() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -401,7 +401,7 @@ async fn allow_owner_make_conseq_transfers() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -517,7 +517,7 @@ async fn allow_conseq_transfers_between_tree_owners() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -593,7 +593,7 @@ async fn allow_conseq_transfers_between_tree_owners() -> anyhow::Result<()> { psbt: issuer_transfer_to_another_resp.psbt.clone(), descriptors: [SecretString(issuer_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; @@ -622,7 +622,7 @@ async fn allow_conseq_transfers_between_tree_owners() -> anyhow::Result<()> { psbt: owner_transfer_to_another_resp.psbt.clone(), descriptors: [SecretString(owner_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 9. Accept Consig (Issuer and Another Owner Sides) @@ -764,7 +764,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { psbt: transfer_resp.psbt.clone(), descriptors: [SecretString(issuer_xprv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -847,7 +847,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { psbt: issuer_transfer_to_another_resp.psbt.clone(), descriptors: [SecretString(issuer_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -876,7 +876,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { psbt: owner_transfer_to_another_resp.psbt.clone(), descriptors: [SecretString(owner_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 9. Accept Consig (Issuer and Another Owner Sides) @@ -956,7 +956,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { psbt: another_transfer_to_issuer.psbt.clone(), descriptors: [SecretString(another_owner_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -1071,7 +1071,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< psbt: transfer_resp.psbt.clone(), descriptors: [SecretString(issuer_xprv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -1154,7 +1154,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< psbt: issuer_transfer_to_another_resp.psbt.clone(), descriptors: [SecretString(issuer_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 9. Create Transfer and Accept (Owner Side) @@ -1182,7 +1182,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< psbt: owner_transfer_to_another_resp.psbt.clone(), descriptors: [SecretString(owner_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -1273,7 +1273,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< psbt: another_transfer_to_issuer.psbt.clone(), descriptors: [SecretString(another_owner_xpriv)].to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -1367,7 +1367,7 @@ async fn allow_issuer_make_transfer_of_two_contracts_in_same_utxo() -> anyhow::R issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -1434,7 +1434,7 @@ async fn allow_issuer_make_transfer_of_two_contracts_in_same_utxo() -> anyhow::R issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.1").await; @@ -1556,7 +1556,7 @@ async fn allow_issuer_make_transfer_of_two_contract_types_in_same_utxo() -> anyh issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 3. Accept Consig (Both Sides) @@ -1626,7 +1626,7 @@ async fn allow_issuer_make_transfer_of_two_contract_types_in_same_utxo() -> anyh issuer_keys.private.rgb_udas_descriptor_xprv.clone(), )], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); // 7. Accept Consig (Both Side) @@ -1968,7 +1968,7 @@ async fn allow_consecutive_full_transfer_bidirectional() -> anyhow::Result<()> { SecretString(wallet_a.private.btc_change_descriptor_xprv.clone()), ], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); let request = AcceptRequest { @@ -2031,7 +2031,7 @@ async fn allow_consecutive_full_transfer_bidirectional() -> anyhow::Result<()> { SecretString(wallet_b.private.btc_change_descriptor_xprv.clone()), ], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); let request = AcceptRequest { @@ -2165,7 +2165,7 @@ async fn allow_save_transfer_and_verify() -> anyhow::Result<()> { SecretString(issuer_keys.private.btc_change_descriptor_xprv.clone()), ], }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); send_some_coins(whatever_address, "0.001").await; diff --git a/tests/rgb/integration/udas.rs b/tests/rgb/integration/udas.rs index db58881f..402a004a 100644 --- a/tests/rgb/integration/udas.rs +++ b/tests/rgb/integration/udas.rs @@ -4,7 +4,7 @@ use crate::rgb::integration::utils::{ issuer_issue_contract_v2, UtxoFilter, ISSUER_MNEMONIC, OWNER_MNEMONIC, }; use bitmask_core::{ - bitcoin::{save_mnemonic, sign_psbt_file}, + bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, rgb::accept_transfer, structs::{AcceptRequest, SecretString, SignPsbtRequest}, }; @@ -61,7 +61,7 @@ async fn allow_beneficiary_accept_transfer() -> anyhow::Result<()> { )] .to_vec(), }; - let resp = sign_psbt_file(request).await; + let resp = sign_and_publish_psbt_file(request).await; assert!(resp.is_ok()); let request = AcceptRequest { diff --git a/tests/rgb/unit/psbt.rs b/tests/rgb/unit/psbt.rs index ebd5911a..1f868e4c 100644 --- a/tests/rgb/unit/psbt.rs +++ b/tests/rgb/unit/psbt.rs @@ -32,6 +32,7 @@ async fn allow_create_psbt_file() -> anyhow::Result<()> { }], vec![], fee, + None, Some("/0/1".to_string()), None, &tx_resolver, diff --git a/tests/rgb/web/swaps.rs b/tests/rgb/web/swaps.rs new file mode 100644 index 00000000..ad58bd92 --- /dev/null +++ b/tests/rgb/web/swaps.rs @@ -0,0 +1,393 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] +#![cfg(target_arch = "wasm32")] +use std::collections::HashMap; +use std::{assert_eq, str::FromStr, vec}; + +use crate::rgb::web::utils::{new_block, send_coins}; +use bdk::blockchain::EsploraBlockchain; +use bitcoin::{consensus, Transaction}; +use bitmask_core::web::constants::sleep; +use bitmask_core::{ + debug, info, + rgb::{prefetch::prefetch_resolver_txs, resolvers::ExplorerResolver}, + structs::{ + AssetType, BatchRgbTransferResponse, ContractResponse, ContractsResponse, + DecryptedWalletData, FullRgbTransferRequest, FundVaultDetails, ImportRequest, + InvoiceRequest, InvoiceResponse, IssueRequest, IssueResponse, NextAddressResponse, + NextUtxoResponse, PsbtFeeRequest, PublishedPsbtResponse, RgbBidRequest, RgbBidResponse, + RgbOfferRequest, RgbOfferResponse, RgbSaveTransferRequest, RgbSwapRequest, RgbSwapResponse, + RgbTransferRequest, RgbTransferResponse, RgbTransferStatusResponse, SecretString, + SignPsbtRequest, SignedPsbtResponse, WalletData, WatcherRequest, WatcherResponse, + }, + web::{ + bitcoin::{ + decrypt_wallet, encrypt_wallet, get_assets_vault, get_new_address, get_wallet_data, + hash_password, new_mnemonic, + }, + json_parse, resolve, + rgb::{ + create_bid, create_offer, create_swap, create_watcher, full_transfer_asset, + get_contract, import_contract, issue_contract, list_contracts, my_bids, my_offers, + my_orders, psbt_sign_and_publish_file, psbt_sign_file, public_offers, + rgb_create_invoice, save_transfer, verify_transfers, watcher_next_address, + watcher_next_utxo, + }, + set_panic_hook, + }, +}; + +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_test::*; +use web_sys::console; + +wasm_bindgen_test_configure!(run_in_browser); + +const ENCRYPTION_PASSWORD: &str = "hunter2"; +const SEED_PASSWORD: &str = ""; + +pub struct TransferRounds { + pub send_amount: u64, + pub satoshi_price: u64, + pub is_issuer_sender: bool, +} + +impl TransferRounds { + pub fn with(send_amount: u64, satoshi_price: u64, is_issuer_sender: bool) -> Self { + TransferRounds { + send_amount, + satoshi_price, + is_issuer_sender, + } + } +} + +#[wasm_bindgen_test] +#[allow(unused_assignments)] +async fn create_transfer_swap_flow() { + set_panic_hook(); + let issuer_vault = resolve(new_mnemonic("".to_string())).await; + let issuer_vault: DecryptedWalletData = json_parse(&issuer_vault); + let owner_vault = resolve(new_mnemonic("".to_string())).await; + let owner_vault: DecryptedWalletData = json_parse(&owner_vault); + + info!("Create Issuer Watcher"); + let iface = "RGB20"; + let watcher_name = "default"; + let issuer_watcher_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: issuer_vault.public.watcher_xpub.clone(), + force: false, + }; + + let issuer_sk = &issuer_vault.private.nostr_prv; + let issuer_watcher_req = serde_wasm_bindgen::to_value(&issuer_watcher_req).expect(""); + let watcher_resp: JsValue = resolve(create_watcher( + issuer_sk.clone(), + issuer_watcher_req.clone(), + )) + .await; + let watcher_resp: WatcherResponse = json_parse(&watcher_resp); + + info!("Get Address"); + let btc_address_1 = resolve(get_new_address( + issuer_vault.public.btc_descriptor_xpub.clone(), + None, + )) + .await; + let btc_address_1: String = json_parse(&btc_address_1); + debug!(format!("Issuer Show Address {}", btc_address_1)); + + let resp = send_coins(&btc_address_1, "1").await; + debug!(format!("Issuer Receive Asset {:?}", resp)); + + let asset_address_1 = resolve(get_new_address( + issuer_vault.public.rgb_assets_descriptor_xpub.clone(), + None, + )) + .await; + let asset_address_1: String = json_parse(&asset_address_1); + debug!(format!("Issuer Show Asset Address {}", asset_address_1)); + + let resp = send_coins(&asset_address_1, "1").await; + debug!(format!("Issuer Receive Asset Bitcoin {:?}", resp)); + + info!("Create Owner Watcher"); + let owner_watcher_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: owner_vault.public.watcher_xpub.clone(), + force: false, + }; + let owner_sk = &owner_vault.private.nostr_prv; + let owner_watcher_req = serde_wasm_bindgen::to_value(&owner_watcher_req).expect(""); + let watcher_resp: JsValue = resolve(create_watcher( + owner_sk.to_string(), + owner_watcher_req.clone(), + )) + .await; + let watcher_resp: WatcherResponse = json_parse(&watcher_resp); + + info!("Get Address"); + let btc_address_1 = resolve(get_new_address( + owner_vault.public.btc_descriptor_xpub.clone(), + None, + )) + .await; + let btc_address_1: String = json_parse(&btc_address_1); + debug!(format!("Owner Show Address {}", btc_address_1)); + + let resp = send_coins(&btc_address_1, "1").await; + debug!(format!("Owner Receive Asset {:?}", resp)); + + let asset_address_1 = resolve(get_new_address( + owner_vault.public.rgb_assets_descriptor_xpub.clone(), + None, + )) + .await; + let asset_address_1: String = json_parse(&asset_address_1); + debug!(format!("Owner Show Asset Address {}", asset_address_1)); + + let resp = send_coins(&asset_address_1, "1").await; + debug!(format!("Owner Receive Asset Bitcoin {:?}", resp)); + + info!("Get UTXO (Owner)"); + let next_utxo: JsValue = resolve(watcher_next_utxo( + owner_sk.clone(), + watcher_name.to_string(), + iface.to_string(), + )) + .await; + let owner_next_utxo: NextUtxoResponse = json_parse(&next_utxo); + debug!(format!("Owner UTXO {:?}", owner_next_utxo.utxo)); + + info!("Get UTXO (Issuer)"); + let next_utxo: JsValue = resolve(watcher_next_utxo( + issuer_sk.clone(), + watcher_name.to_string(), + iface.to_string(), + )) + .await; + let issuer_next_utxo: NextUtxoResponse = json_parse(&next_utxo); + debug!(format!("Issuer UTXO {:?}", issuer_next_utxo.utxo)); + + assert!(issuer_next_utxo.utxo.is_some()); + assert!(owner_next_utxo.utxo.is_some()); + + info!("Create Contract (Issuer)"); + let supply = 5000; + let issue_utxo = issuer_next_utxo.utxo.unwrap().outpoint.to_string(); + let issue_seal = format!("tapret1st:{issue_utxo}"); + let issue_req = IssueRequest { + ticker: "DIBA".to_string(), + name: "DIBA".to_string(), + description: "DIBA".to_string(), + precision: 2, + supply, + seal: issue_seal.to_owned(), + iface: iface.to_string(), + meta: None, + }; + + let issue_req = serde_wasm_bindgen::to_value(&issue_req).expect(""); + let issue_resp: JsValue = resolve(issue_contract(issuer_sk.to_string(), issue_req)).await; + let issuer_resp: IssueResponse = json_parse(&issue_resp); + + let mut total_issuer = supply; + let mut total_owner = 0; + let rounds = vec![TransferRounds::with(4000, 1_000, true)]; + + let mut sender = String::new(); + let mut sender_sk = String::new(); + let mut sender_desc = String::new(); + let mut sender_keys = vec![]; + + let mut receiver = String::new(); + let mut receiver_sk = String::new(); + let mut receiver_desc = String::new(); + let mut receiver_keys = vec![]; + + for (index, round) in rounds.into_iter().enumerate() { + if round.is_issuer_sender { + sender = "ISSUER".to_string(); + sender_sk = issuer_sk.to_string(); + sender_desc = issuer_vault.public.rgb_assets_descriptor_xpub.to_string(); + sender_keys = vec![ + SecretString(issuer_vault.private.rgb_assets_descriptor_xprv.clone()), + SecretString(issuer_vault.private.btc_descriptor_xprv.clone()), + SecretString(issuer_vault.private.btc_change_descriptor_xprv.clone()), + ]; + + receiver = "OWNER".to_string(); + receiver_sk = owner_sk.to_string(); + receiver_desc = owner_vault.public.rgb_assets_descriptor_xpub.to_string(); + receiver_keys = vec![ + SecretString(owner_vault.private.rgb_assets_descriptor_xprv.clone()), + SecretString(owner_vault.private.btc_descriptor_xprv.clone()), + SecretString(owner_vault.private.btc_change_descriptor_xprv.clone()), + ]; + } else { + sender = "OWNER".to_string(); + sender_sk = owner_sk.to_string(); + sender_desc = owner_vault.public.rgb_assets_descriptor_xpub.to_string(); + sender_keys = vec![ + SecretString(owner_vault.private.rgb_assets_descriptor_xprv.clone()), + SecretString(owner_vault.private.btc_descriptor_xprv.clone()), + SecretString(owner_vault.private.btc_change_descriptor_xprv.clone()), + ]; + + receiver = "ISSUER".to_string(); + receiver_sk = issuer_sk.to_string(); + receiver_desc = issuer_vault.public.rgb_assets_descriptor_xpub.to_string(); + receiver_keys = vec![ + SecretString(issuer_vault.private.rgb_assets_descriptor_xprv.clone()), + SecretString(issuer_vault.private.btc_descriptor_xprv.clone()), + SecretString(issuer_vault.private.btc_change_descriptor_xprv.clone()), + ]; + } + + info!(format!( + ">>>> ROUND #{index} {sender} SWAP {} units to {receiver} <<<<", + round.send_amount + )); + + info!(format!("Sender ({sender}) Create Offer")); + let expire_at = (chrono::Local::now() + chrono::Duration::minutes(5)) + .naive_utc() + .timestamp(); + let sender_asset_desc = sender_desc.clone(); + let sender_swap_req = RgbOfferRequest { + contract_id: issuer_resp.contract_id.clone(), + iface: issuer_resp.iface.clone(), + contract_amount: round.send_amount, + bitcoin_price: round.satoshi_price, + descriptor: SecretString(sender_asset_desc), + change_terminal: "/20/1".to_string(), + bitcoin_changes: vec![], + expire_at: Some(expire_at), + }; + let sender_swap_req = serde_wasm_bindgen::to_value(&sender_swap_req).expect(""); + + let sender_swap_resp: JsValue = + resolve(create_offer(sender_sk.clone(), sender_swap_req)).await; + let sender_swap_resp: RgbOfferResponse = json_parse(&sender_swap_resp); + + info!(format!("Receiver ({receiver}) Create Bid")); + let receiver_btc_desc = receiver_desc.clone(); + let receiver_swap_req = RgbBidRequest { + offer_id: sender_swap_resp.offer_id.clone(), + asset_amount: round.send_amount, + descriptor: SecretString(receiver_btc_desc), + change_terminal: "/1/0".to_string(), + fee: PsbtFeeRequest::Value(1000), + }; + let receiver_swap_req = serde_wasm_bindgen::to_value(&receiver_swap_req).expect(""); + + let receiver_swap_resp: JsValue = + resolve(create_bid(receiver_sk.clone(), receiver_swap_req)).await; + let receiver_swap_resp: RgbBidResponse = json_parse(&receiver_swap_resp); + + info!(format!("Receiver ({receiver}) Sign Bid")); + let psbt_req = SignPsbtRequest { + psbt: receiver_swap_resp.swap_psbt, + descriptors: receiver_keys.clone(), + }; + + let receiver_psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); + let receiver_psbt_resp: JsValue = + resolve(psbt_sign_file(receiver_sk.clone(), receiver_psbt_req)).await; + let receiver_psbt_resp: SignedPsbtResponse = json_parse(&receiver_psbt_resp); + debug!(format!("Sign Bid Psbt: {}", receiver_psbt_resp.sign)); + + info!(format!("Sender ({sender}) Create Swap")); + let final_swap_req = RgbSwapRequest { + offer_id: sender_swap_resp.offer_id.clone(), + bid_id: receiver_swap_resp.bid_id, + swap_psbt: receiver_psbt_resp.psbt, + }; + let final_swap_req = serde_wasm_bindgen::to_value(&final_swap_req).expect(""); + + let final_swap_res: JsValue = resolve(create_swap(sender_sk.clone(), final_swap_req)).await; + let final_swap_res: RgbSwapResponse = json_parse(&final_swap_res); + + info!(format!("Sender ({sender}) Sign Swap")); + let psbt_req = SignPsbtRequest { + psbt: final_swap_res.final_psbt, + descriptors: sender_keys, + }; + + let swap_psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); + let swap_psbt_resp: JsValue = + resolve(psbt_sign_and_publish_file(sender_sk.clone(), swap_psbt_req)).await; + let swap_psbt_resp: PublishedPsbtResponse = json_parse(&swap_psbt_resp); + debug!(format!("Sign & Publish Psbt: {:?}", swap_psbt_resp)); + + info!("Create new Block"); + let resp = new_block().await; + debug!(format!("Block Created: {:?}", resp)); + + info!(format!("Save Consig ({receiver})")); + let save_transfer_req = RgbSaveTransferRequest { + iface: issuer_resp.iface.clone(), + consignment: final_swap_res.final_consig.clone(), + }; + let save_transfer_req = serde_wasm_bindgen::to_value(&save_transfer_req).expect(""); + let save_transfer_resp = + resolve(save_transfer(receiver_sk.to_string(), save_transfer_req)).await; + let save_transfer_resp: RgbTransferStatusResponse = json_parse(&save_transfer_resp); + debug!(format!("Save Consig: {:?}", save_transfer_resp)); + + info!("Verify Consig (Both)"); + let verify_transfer_resp = resolve(verify_transfers(sender_sk.clone().to_string())).await; + let verify_transfer_resp: BatchRgbTransferResponse = json_parse(&verify_transfer_resp); + debug!(format!( + "Verify Consig ({sender}): {:?}", + verify_transfer_resp + )); + + let verify_transfer_resp = resolve(verify_transfers(receiver_sk.clone())).await; + let verify_transfer_resp: BatchRgbTransferResponse = json_parse(&verify_transfer_resp); + debug!(format!( + "Verify Consig ({receiver}): {:?}", + verify_transfer_resp + )); + + let (sender_balance, receiver_balance) = if round.is_issuer_sender { + total_issuer -= round.send_amount; + total_owner += round.send_amount; + (total_issuer, total_owner) + } else { + total_issuer += round.send_amount; + total_owner -= round.send_amount; + (total_owner, total_issuer) + }; + + info!(format!("Get Contract Balancer ({sender})")); + let contract_resp = resolve(get_contract( + sender_sk.to_string(), + issuer_resp.contract_id.clone(), + )) + .await; + let contract_resp: ContractResponse = json_parse(&contract_resp); + debug!(format!( + "Contract ({sender}): {} ({})\n {:#?}", + contract_resp.contract_id, contract_resp.balance, contract_resp.allocations + )); + let sender_current_balance = contract_resp.balance; + + info!(format!("Get Contract Balancer ({receiver})")); + let contract_resp = + resolve(get_contract(receiver_sk, issuer_resp.contract_id.clone())).await; + let contract_resp: ContractResponse = json_parse(&contract_resp); + debug!(format!( + "Contract ({receiver}): {} ({})\n {:#?}", + contract_resp.contract_id, contract_resp.balance, contract_resp.allocations + )); + let receiver_current_balance = contract_resp.balance; + + info!(format!("<<<< ROUND #{index} Finish >>>>")); + assert_eq!(sender_current_balance, sender_balance); + assert_eq!(receiver_current_balance, receiver_balance); + } +} diff --git a/tests/rgb/web/transfers.rs b/tests/rgb/web/transfers.rs index a22ad1ce..7a50a6d9 100644 --- a/tests/rgb/web/transfers.rs +++ b/tests/rgb/web/transfers.rs @@ -16,9 +16,9 @@ use bitmask_core::{ AssetType, BatchRgbTransferResponse, ContractResponse, ContractsResponse, DecryptedWalletData, FullRgbTransferRequest, FundVaultDetails, ImportRequest, InvoiceRequest, InvoiceResponse, IssueRequest, IssueResponse, NextAddressResponse, - NextUtxoResponse, PsbtFeeRequest, RgbSaveTransferRequest, RgbTransferRequest, - RgbTransferResponse, RgbTransferStatusResponse, SecretString, SignPsbtRequest, - SignPsbtResponse, WalletData, WatcherRequest, WatcherResponse, + NextUtxoResponse, PsbtFeeRequest, PublishedPsbtResponse, RgbSaveTransferRequest, + RgbTransferRequest, RgbTransferResponse, RgbTransferStatusResponse, SecretString, + SignPsbtRequest, WalletData, WatcherRequest, WatcherResponse, }, web::{ bitcoin::{ @@ -28,8 +28,8 @@ use bitmask_core::{ json_parse, resolve, rgb::{ create_watcher, full_transfer_asset, get_contract, import_contract, issue_contract, - list_contracts, psbt_sign_file, rgb_create_invoice, save_transfer, verify_transfers, - watcher_next_address, watcher_next_utxo, + list_contracts, psbt_sign_and_publish_file, rgb_create_invoice, save_transfer, + verify_transfers, watcher_next_address, watcher_next_utxo, }, set_panic_hook, }, @@ -315,8 +315,9 @@ async fn create_transfer_with_fee_value() { }; let psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); - let psbt_resp: JsValue = resolve(psbt_sign_file(sender_sk.to_string(), psbt_req)).await; - let psbt_resp: SignPsbtResponse = json_parse(&psbt_resp); + let psbt_resp: JsValue = + resolve(psbt_sign_and_publish_file(sender_sk.to_string(), psbt_req)).await; + let psbt_resp: PublishedPsbtResponse = json_parse(&psbt_resp); debug!(format!("Sign Psbt: {:?}", psbt_resp)); info!("Create new Block"); @@ -603,8 +604,9 @@ async fn create_transfer_with_fee_rate() { }; let psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); - let psbt_resp: JsValue = resolve(psbt_sign_file(sender_sk.to_string(), psbt_req)).await; - let psbt_resp: SignPsbtResponse = json_parse(&psbt_resp); + let psbt_resp: JsValue = + resolve(psbt_sign_and_publish_file(sender_sk.to_string(), psbt_req)).await; + let psbt_resp: PublishedPsbtResponse = json_parse(&psbt_resp); debug!(format!("Sign Psbt: {:?}", psbt_resp)); info!("Create new Block");