diff --git a/Cargo.lock b/Cargo.lock index 0022924c..7e65a52d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,52 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "automerge" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6738a70b192adaf8f5393fa286a994393cbfc4b2cb780697d2c423f45c260ab" +dependencies = [ + "flate2", + "fxhash", + "hex", + "itertools", + "leb128", + "serde", + "sha2 0.10.7", + "smol_str", + "thiserror", + "tinyvec", + "tracing", + "uuid", +] + +[[package]] +name = "autosurgeon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e5a84cd8615e4baa36c9f14b14f9b310a524e92c16329e3f6684525f466502" +dependencies = [ + "automerge", + "autosurgeon-derive", + "similar", + "smol_str", + "thiserror", +] + +[[package]] +name = "autosurgeon-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91bccbe0b61b69be46a0c461c1b287d8a8465a8c83cf770d2a34dd3dc70d8c7e" +dependencies = [ + "proc-macro2", + "quote", + "smol_str", + "syn 2.0.26", + "thiserror", +] + [[package]] name = "axum" version = "0.6.18" @@ -589,11 +635,13 @@ checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "bitmask-core" -version = "0.6.3-rc.15" +version = "0.6.3-rc.16" dependencies = [ "amplify", "anyhow", "argon2", + "automerge", + "autosurgeon", "axum", "axum-macros", "base64-compat", @@ -620,7 +668,7 @@ dependencies = [ "getrandom", "gloo-console", "gloo-net", - "gloo-utils", + "gloo-utils 0.2.0", "hex", "indexmap 1.9.3", "inflate", @@ -1409,6 +1457,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "garde" version = "0.11.2" @@ -1474,7 +1531,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" dependencies = [ - "gloo-utils", + "gloo-utils 0.1.7", "js-sys", "serde", "wasm-bindgen", @@ -1490,7 +1547,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.1.7", "http", "js-sys", "pin-project", @@ -1527,6 +1584,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "h2" version = "0.3.20" @@ -1900,6 +1970,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1921,6 +2000,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "libc" version = "0.2.147" @@ -3165,6 +3250,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + [[package]] name = "single_use_seals" version = "0.10.0" @@ -3200,6 +3291,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + [[package]] name = "snap" version = "1.1.0" @@ -3640,9 +3740,21 @@ dependencies = [ "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -3775,6 +3887,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index b76b461b..39c0c65f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitmask-core" -version = "0.6.3-rc.15" +version = "0.6.3-rc.16" authors = [ "Jose Diego Robles ", "Hunter Trujillo ", @@ -87,6 +87,8 @@ tokio = { version = "1.28.2", features = ["macros", "sync"] } zeroize = "1.6.0" blake3 = "1.4.1" base85 = "2.0.0" +automerge = "0.5.1" +autosurgeon = "0.8" [target.'cfg(target_arch = "wasm32")'.dependencies] bdk = { version = "0.28.0", features = [ @@ -95,7 +97,7 @@ bdk = { version = "0.28.0", features = [ ], default-features = false } gloo-console = "0.2.3" gloo-net = { version = "0.3.1", features = ["http"] } -gloo-utils = "0.1.7" +gloo-utils = "0.2.0" js-sys = "0.3.63" serde-wasm-bindgen = "0.5.0" wasm-bindgen = { version = "0.2.86", features = ["serde-serialize"] } diff --git a/src/bin/bitmaskd.rs b/src/bin/bitmaskd.rs index 3ebae3f7..4bea98fe 100644 --- a/src/bin/bitmaskd.rs +++ b/src/bin/bitmaskd.rs @@ -3,10 +3,9 @@ #![cfg(not(target_arch = "wasm32"))] use std::{env, fs::OpenOptions, io::ErrorKind, net::SocketAddr, str::FromStr}; -use amplify::hex::ToHex; use anyhow::Result; use axum::{ - body::{Bytes, Full}, + body::Bytes, extract::Path, headers::{authorization::Bearer, Authorization, CacheControl}, http::StatusCode, @@ -16,8 +15,8 @@ use axum::{ }; use bitcoin_30::secp256k1::{ecdh::SharedSecret, PublicKey, SecretKey}; use bitmask_core::{ - bitcoin::{decrypt_wallet, get_wallet_data, save_mnemonic, sign_psbt_file}, - carbonado::{handle_file, retrieve, retrieve_metadata}, + bitcoin::{save_mnemonic, sign_psbt_file}, + carbonado::handle_file, constants::{get_marketplace_seed, get_network, get_udas_utxo, switch_network}, rgb::{ accept_transfer, clear_watcher as rgb_clear_watcher, create_invoice, create_psbt, @@ -29,16 +28,12 @@ use bitmask_core::{ }, structs::{ AcceptRequest, FileMetadata, FullRgbTransferRequest, ImportRequest, InvoiceRequest, - IssueAssetRequest, IssueRequest, MediaInfo, PsbtFeeRequest, PsbtRequest, ReIssueRequest, - RgbRemoveTransferRequest, RgbSaveTransferRequest, RgbTransferRequest, SecretString, - SelfFullRgbTransferRequest, SelfInvoiceRequest, SelfIssueRequest, SignPsbtRequest, - WatcherRequest, + IssueRequest, PsbtFeeRequest, PsbtRequest, ReIssueRequest, RgbRemoveTransferRequest, + RgbSaveTransferRequest, RgbTransferRequest, SecretString, SelfFullRgbTransferRequest, + SelfInvoiceRequest, SelfIssueRequest, SignPsbtRequest, WatcherRequest, }, }; -use carbonado::file; use log::{debug, error, info}; -use rgbstd::interface::Iface; -use serde::{Deserialize, Serialize}; use tokio::fs; use tower_http::cors::CorsLayer; @@ -636,10 +631,10 @@ async fn main() -> Result<()> { .route("/transfers/", delete(remove_transfer)) .route("/key/:pk", get(key)) .route("/carbonado/status", get(status)) + .route("/carbonado/:pk/:name", get(co_retrieve)) .route("/carbonado/:pk/:name", post(co_store)) .route("/carbonado/:pk/:name/force", post(co_force_store)) - .route("/carbonado/:pk/:name/metadata", get(co_metadata)) - .route("/carbonado/:pk/:name", get(co_retrieve)); + .route("/carbonado/:pk/:name/metadata", get(co_metadata)); let network = get_network().await; switch_network(&network).await?; diff --git a/src/carbonado.rs b/src/carbonado.rs index 7c13fbf8..72698310 100644 --- a/src/carbonado.rs +++ b/src/carbonado.rs @@ -6,13 +6,7 @@ use crate::{carbonado::error::CarbonadoError, constants::NETWORK, info, structs: pub mod error; #[cfg(not(target_arch = "wasm32"))] -pub use server::handle_file; -#[cfg(not(target_arch = "wasm32"))] -pub use server::retrieve; -#[cfg(not(target_arch = "wasm32"))] -pub use server::retrieve_metadata; -#[cfg(not(target_arch = "wasm32"))] -pub use server::store; +pub use server::{handle_file, retrieve, retrieve_metadata, store}; #[cfg(not(target_arch = "wasm32"))] mod server { @@ -46,33 +40,6 @@ mod server { Ok(()) } - pub async fn retrieve_metadata(sk: &str, name: &str) -> Result { - let sk = hex::decode(sk)?; - let secret_key = SecretKey::from_slice(&sk)?; - let public_key = PublicKey::from_secret_key_global(&secret_key); - let pk = public_key.to_hex(); - - let network = NETWORK.read().await.to_string(); - let networks = ["bitcoin", "mainnet", "testnet", "signet", "regtest"]; - - let mut final_name = name.to_string(); - if !networks.into_iter().any(|x| name.contains(x)) { - final_name = format!("{network}-{name}"); - } - - let filepath = handle_file(&pk, &final_name, 0).await?; - let bytes = fs::read(filepath).await?; - - let (header, _) = carbonado::file::decode(&sk, &bytes)?; - - let result = FileMetadata { - filename: header.file_name(), - metadata: header.metadata.unwrap_or_default(), - }; - - Ok(result) - } - pub async fn retrieve( sk: &str, name: &str, @@ -87,7 +54,7 @@ mod server { let mut final_name = name.to_string(); let network = NETWORK.read().await.to_string(); - let networks = ["bitcoin", "mainnet", "testnet", "signet", "regtest"]; + let networks = ["bitcoin", "testnet", "signet", "regtest"]; if !networks.into_iter().any(|x| name.contains(x)) { final_name = format!("{network}-{name}"); } @@ -122,7 +89,7 @@ mod server { ) -> Result { let mut final_name = name.to_string(); let network = NETWORK.read().await.to_string(); - let networks = ["bitcoin", "mainnet", "testnet", "signet", "regtest"]; + let networks = ["bitcoin", "testnet", "signet", "regtest"]; if !networks.into_iter().any(|x| name.contains(x)) { final_name = format!("{network}-{name}"); } @@ -151,28 +118,50 @@ mod server { Ok(filepath) } + + pub async fn retrieve_metadata(sk: &str, name: &str) -> Result { + let sk = hex::decode(sk)?; + let secret_key = SecretKey::from_slice(&sk)?; + let public_key = PublicKey::from_secret_key_global(&secret_key); + let pk = public_key.to_hex(); + + let network = NETWORK.read().await.to_string(); + let networks = ["bitcoin", "testnet", "signet", "regtest"]; + + let mut final_name = name.to_string(); + if !networks.into_iter().any(|x| name.contains(x)) { + final_name = format!("{network}-{name}"); + } + + let filepath = handle_file(&pk, &final_name, 0).await?; + let bytes = fs::read(filepath).await?; + + let (header, _) = carbonado::file::decode(&sk, &bytes)?; + + let result = FileMetadata { + filename: header.file_name(), + metadata: header.metadata.unwrap_or_default(), + }; + + Ok(result) + } } #[cfg(target_arch = "wasm32")] -pub use client::retrieve; -#[cfg(target_arch = "wasm32")] -pub use client::retrieve_metadata; -#[cfg(target_arch = "wasm32")] -pub use client::store; +pub use client::{retrieve, retrieve_metadata, store}; #[cfg(target_arch = "wasm32")] mod client { use super::*; - - use std::sync::Arc; - - use gloo_net::http::Request; - use gloo_utils::errors::JsError; use js_sys::{Array, Promise, Uint8Array}; use serde::Deserialize; + use std::sync::Arc; use wasm_bindgen::JsValue; use wasm_bindgen_futures::{future_to_promise, JsFuture}; + use gloo_net::http::Request; + use gloo_utils::errors::JsError; + use crate::constants::CARBONADO_ENDPOINT; fn js_to_error(js_value: JsValue) -> CarbonadoError { @@ -191,102 +180,6 @@ mod client { value: f64, } - // #[derive(Debug, Deserialize)] - // struct GetRetrievePromiseResult { - // value: Vec, - // } - - async fn fetch_post(url: String, body: Arc>) -> Result { - let array = Uint8Array::new_with_length(body.len() as u32); - array.copy_from(&body); - - let request = Request::post(&url) - .header("Content-Type", "application/octet-stream") - .header("Cache-Control", "no-cache") - .body(array); - - let request = match request { - Ok(request) => request, - Err(e) => return Err(JsValue::from(e.to_string())), - }; - - let response = request.send().await; - - match response { - Ok(response) => { - let status_code = response.status(); - if status_code == 200 { - Ok(JsValue::from(status_code)) - } else { - Err(JsValue::from(status_code)) - } - } - Err(e) => Err(JsValue::from(e.to_string())), - } - } - - async fn fetch_get_text(url: String) -> Result { - let request = Request::get(&url) - .header("Content-Type", "application/octet-stream") - .header("Cache-Control", "no-cache") - .build(); - - let request = match request { - Ok(request) => request, - Err(e) => return Err(JsValue::from(e.to_string())), - }; - - let response = request.send().await; - - match response { - Ok(response) => { - let status_code = response.status(); - if status_code == 200 { - match response.text().await { - Ok(text) => Ok(JsValue::from(&text)), - Err(e) => Err(JsValue::from(e.to_string())), - } - } else { - Err(JsValue::from(status_code)) - } - } - Err(e) => Err(JsValue::from(e.to_string())), - } - } - - async fn fetch_get_byte_array(url: String) -> Result { - let request = Request::get(&url) - .header("Content-Type", "application/octet-stream") - .header("Cache-Control", "no-cache") - .build(); - - let request = match request { - Ok(request) => request, - Err(e) => return Err(JsValue::from(e.to_string())), - }; - - let response = request.send().await; - - match response { - Ok(response) => { - let status_code = response.status(); - if status_code == 200 { - match response.binary().await { - Ok(bytes) => { - let array = Uint8Array::new_with_length(bytes.len() as u32); - array.copy_from(&bytes); - Ok(JsValue::from(&array)) - } - Err(e) => Err(JsValue::from(e.to_string())), - } - } else { - Err(JsValue::from(status_code)) - } - } - Err(e) => Err(JsValue::from(e.to_string())), - } - } - pub async fn store( sk: &str, name: &str, @@ -422,6 +315,97 @@ mod client { Ok((Vec::new(), None)) } + + async fn fetch_post(url: String, body: Arc>) -> Result { + let array = Uint8Array::new_with_length(body.len() as u32); + array.copy_from(&body); + + let request = Request::post(&url) + .header("Content-Type", "application/octet-stream") + .header("Cache-Control", "no-cache") + .body(array); + + let request = match request { + Ok(request) => request, + Err(e) => return Err(JsValue::from(e.to_string())), + }; + + let response = request.send().await; + + match response { + Ok(response) => { + let status_code = response.status(); + if status_code == 200 { + Ok(JsValue::from(status_code)) + } else { + Err(JsValue::from(status_code)) + } + } + Err(e) => Err(JsValue::from(e.to_string())), + } + } + + async fn fetch_get_text(url: String) -> Result { + let request = Request::get(&url) + .header("Content-Type", "application/octet-stream") + .header("Cache-Control", "no-cache") + .build(); + + let request = match request { + Ok(request) => request, + Err(e) => return Err(JsValue::from(e.to_string())), + }; + + let response = request.send().await; + + match response { + Ok(response) => { + let status_code = response.status(); + if status_code == 200 { + match response.text().await { + Ok(text) => Ok(JsValue::from(&text)), + Err(e) => Err(JsValue::from(e.to_string())), + } + } else { + Err(JsValue::from(status_code)) + } + } + Err(e) => Err(JsValue::from(e.to_string())), + } + } + + async fn fetch_get_byte_array(url: String) -> Result { + let request = Request::get(&url) + .header("Content-Type", "application/octet-stream") + .header("Cache-Control", "no-cache") + .build(); + + let request = match request { + Ok(request) => request, + Err(e) => return Err(JsValue::from(e.to_string())), + }; + + let response = request.send().await; + + match response { + Ok(response) => { + let status_code = response.status(); + if status_code == 200 { + match response.binary().await { + Ok(bytes) => { + let array = Uint8Array::new_with_length(bytes.len() as u32); + array.copy_from(&bytes); + Ok(JsValue::from(&array)) + } + Err(e) => Err(JsValue::from(e.to_string())), + } + } else { + Err(JsValue::from(status_code)) + } + } + Err(e) => Err(JsValue::from(e.to_string())), + } + } } // Utility functions for handling data of different encodings diff --git a/src/rgb.rs b/src/rgb.rs index 19d9f5de..2adb3e2b 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -3,14 +3,14 @@ use amplify::{ confinement::U32, hex::{FromHex, ToHex}, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; +use autosurgeon::reconcile; use bitcoin::{Network, Txid}; use bitcoin_30::bip32::ExtendedPubKey; use bitcoin_scripts::address::AddressNetwork; use garde::Validate; use miniscript_crate::DescriptorPublicKey; -use rand::{rngs::StdRng, Rng, SeedableRng}; -use rgb::{RgbDescr, SpkDescriptor, TerminalPath}; +use rgb::RgbDescr; use rgbstd::{ containers::BindleContent, contract::ContractId, @@ -30,8 +30,11 @@ pub mod accept; pub mod carbonado; pub mod constants; pub mod contract; +pub mod crdt; +pub mod fs; pub mod import; pub mod issue; +pub mod prebuild; pub mod prefetch; pub mod psbt; pub mod resolvers; @@ -40,14 +43,8 @@ pub mod transfer; pub mod wallet; use crate::{ - constants::{ - get_network, - storage_keys::{ASSETS_STOCK, ASSETS_TRANSFERS, ASSETS_WALLETS}, - BITCOIN_EXPLORER_API, NETWORK, - }, + constants::{get_network, BITCOIN_EXPLORER_API, NETWORK}, rgb::{ - carbonado::{force_store_stock, retrieve_stock, store_stock}, - constants::WALLET_UNAVAILABLE, issue::{issue_contract as create_contract, IssueContractError}, psbt::{create_psbt as create_rgb_psbt, extract_commit}, resolvers::ExplorerResolver, @@ -58,29 +55,33 @@ use crate::{ wallet::list_allocations, }, structs::{ - AcceptRequest, AcceptResponse, AllocationDetail, AllocationValue, AssetType, - BatchRgbTransferItem, BatchRgbTransferResponse, ContractMetadata, ContractResponse, - ContractsResponse, FullRgbTransferRequest, ImportRequest, InterfaceDetail, - InterfacesResponse, InvoiceRequest, InvoiceResponse, IssueMetaRequest, IssueMetadata, - IssueRequest, IssueResponse, NewCollectible, NextAddressResponse, NextUtxoResponse, - NextUtxosResponse, PsbtFeeRequest, PsbtInputRequest, PsbtRequest, PsbtResponse, - ReIssueRequest, ReIssueResponse, RgbInvoiceResponse, RgbRemoveTransferRequest, - RgbSaveTransferRequest, RgbTransferDetail, RgbTransferRequest, RgbTransferResponse, - RgbTransferStatusResponse, RgbTransfersResponse, SchemaDetail, SchemasResponse, - SecretString, TransferType, TxStatus, UDADetail, UtxoResponse, WatcherDetailResponse, + AcceptRequest, AcceptResponse, AssetType, BatchRgbTransferItem, BatchRgbTransferResponse, + ContractMetadata, ContractResponse, ContractsResponse, FullRgbTransferRequest, + 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, }, validators::RGBContext, }; use self::{ - carbonado::{retrieve_transfers, retrieve_wallets, store_transfers, store_wallets}, - constants::{ - BITCOIN_DEFAULT_FETCH_LIMIT, CARBONADO_UNAVAILABLE, RGB_DEFAULT_FETCH_LIMIT, - RGB_DEFAULT_NAME, STOCK_UNAVAILABLE, TRANSFER_UNAVAILABLE, - }, + constants::{RGB_DEFAULT_FETCH_LIMIT, RGB_DEFAULT_NAME}, 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, + }, import::{import_contract, ImportContractError}, + prebuild::prebuild_transfer_asset, prefetch::{ prefetch_resolver_allocations, prefetch_resolver_images, prefetch_resolver_import_rgb, prefetch_resolver_psbt, prefetch_resolver_rgb, prefetch_resolver_txs_status, @@ -88,11 +89,11 @@ use self::{ prefetch_resolver_wutxo, }, psbt::{fee_estimate, save_commit, CreatePsbtError}, - structs::{AddressAmount, RgbTransfer, RgbTransfers}, + structs::{RgbAccount, RgbTransfer, RgbTransfers}, transfer::{extract_transfer, AcceptTransferError, NewInvoiceError, NewPaymentError}, wallet::{ - create_wallet, get_address, next_address, next_utxo, next_utxos, register_address, - register_utxo, sync_wallet, + create_wallet, next_address, next_utxo, next_utxos, register_address, register_utxo, + sync_wallet, }, }; @@ -101,10 +102,8 @@ use self::{ pub enum IssueError { /// Some request data is missing. {0:?} Validation(BTreeMap), - /// Retrieve I/O or connectivity error. {1} in {0} - Retrieve(String, String), - /// Write I/O or connectivity error. {1} in {0} - Write(String, String), + /// I/O or connectivity error. {0} + IO(RgbPersistenceError), /// Watcher is required for this operation. Watcher, /// Occurs an error in issue step. {0} @@ -135,25 +134,12 @@ pub async fn issue_contract(sk: &str, request: IssueRequest) -> Result Result = allocations @@ -331,12 +287,9 @@ pub async fn reissue_contract( let UDADetail { ticker, name, - token_index: _, description, - balance: _, media, - allocations: _, - attach: _, + .. } = collectible_item; let new_item = NewCollectible { @@ -399,13 +352,11 @@ pub async fn reissue_contract( name, description, supply, - precision: _, - balance: _, - allocations: _, contract, genesis, meta, created, + .. } = export_contract( contract.contract_id(), &mut stock, @@ -438,23 +389,9 @@ pub async fn reissue_contract( }); } - force_store_stock(sk, ASSETS_STOCK, &stock) + store_stock_account(sk, stock, rgb_account) .await - .map_err(|_| { - IssueError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - - store_wallets(sk, ASSETS_WALLETS, &rgb_account) - .await - .map_err(|_| { - IssueError::Write( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.to_string(), - ) - })?; + .map_err(IssueError::IO)?; Ok(ReIssueResponse { contracts: reissue_resp, @@ -466,10 +403,8 @@ pub async fn reissue_contract( pub enum InvoiceError { /// Some request data is missing. {0:?} Validation(BTreeMap), - /// Retrieve I/O or connectivity error. {1} in {0} - Retrive(String, String), - /// Write I/O or connectivity error. {1} in {0} - Write(String, String), + /// I/O or connectivity error. {0} + IO(RgbPersistenceError), /// Occurs an error in invoice step. {0} Invoice(NewInvoiceError), } @@ -495,22 +430,11 @@ pub async fn create_invoice( params, } = request; - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await.map_err(|_| { - InvoiceError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - + let mut stock = retrieve_rgb_stock(sk).await.map_err(InvoiceError::IO)?; let invoice = create_rgb_invoice(&contract_id, &iface, amount, &seal, params, &mut stock) .map_err(InvoiceError::Invoice)?; - store_stock(sk, ASSETS_STOCK, &stock).await.map_err(|_| { - InvoiceError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; + store_rgb_stock(sk, stock).await.map_err(InvoiceError::IO)?; Ok(InvoiceResponse { invoice: invoice.to_string(), @@ -522,16 +446,16 @@ pub async fn create_invoice( pub enum TransferError { /// Some request data is missing. {0:?} Validation(BTreeMap), - /// Retrieve I/O or connectivity error. {1} in {0} - Retrive(String, String), - /// Write I/O or connectivity error. {1} in {0} - Write(String, String), + /// 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, + /// Auto merge fail in this opration + WrongAutoMerge(String), /// Occurs an error in create step. {0} Create(CreatePsbtError), /// Occurs an error in commitment step. {0} @@ -551,6 +475,21 @@ pub enum TransferError { } 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?; + Ok(psbt) +} + +async fn internal_create_psbt( + request: PsbtRequest, + rgb_account: &mut RgbAccount, + resolver: &mut ExplorerResolver, +) -> Result { if let Err(err) = request.validate(&RGBContext::default()) { let errors = err .flatten() @@ -560,6 +499,10 @@ pub async fn create_psbt(sk: &str, request: PsbtRequest) -> Result Result Result Result Result Result { if let Err(err) = request.validate(&RGBContext::default()) { let errors = err @@ -652,30 +564,116 @@ pub async fn transfer_asset( return Err(TransferError::Validation(errors)); } - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await.map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; + let (mut stock, mut rgb_transfers) = retrieve_stock_transfers(sk) + .await + .map_err(TransferError::IO)?; - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await.map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.to_string(), - ) - })?; + let local_rgb_account = retrieve_local_account(sk) + .await + .map_err(TransferError::IO)?; + + let LocalRgbAccount { + doc, + mut rgb_account, + } = local_rgb_account; + let mut fork_wallet = automerge::AutoCommit::load(&doc) + .map_err(|op| TransferError::WrongAutoMerge(op.to_string()))?; + let mut rgb_account_changes = RawRgbAccount::from(rgb_account.clone()); + + let mut resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..Default::default() + }; + + let mut rgb_wallet = match rgb_account.wallets.get(RGB_DEFAULT_NAME) { + Some(rgb_wallet) => rgb_wallet.to_owned(), + _ => return Err(TransferError::NoWatcher), + }; + + let (asset_inputs, bitcoin_inputs, bitcoin_changes) = + prebuild_transfer_asset(request.clone(), &mut stock, &mut rgb_wallet, &mut resolver) + .await?; + + let FullRgbTransferRequest { + rgb_invoice, + change_terminal, + fee, + .. + } = request; + + let psbt_req = PsbtRequest { + fee, + asset_inputs, + bitcoin_inputs, + bitcoin_changes, + asset_descriptor_change: None, + asset_terminal_change: Some(change_terminal), + }; + + let psbt_response = internal_create_psbt(psbt_req, &mut rgb_account, &mut resolver).await?; + let transfer_req = RgbTransferRequest { + rgb_invoice, + psbt: psbt_response.psbt, + terminal: psbt_response.terminal, + }; + + let resp = internal_transfer_asset( + transfer_req, + &mut stock, + &mut rgb_account, + &mut rgb_transfers, + ) + .await?; - let mut rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) + rgb_account.clone().update(&mut rgb_account_changes); + reconcile(&mut fork_wallet, rgb_account_changes.clone()) + .map_err(|op| TransferError::WrongAutoMerge(op.to_string()))?; + + store_local_account(sk, fork_wallet.save()) .await - .map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - TRANSFER_UNAVAILABLE.to_string(), - ) - })?; + .map_err(TransferError::IO)?; - if rgb_account.wallets.get("default").is_none() { + store_stock_transfers(sk, stock, rgb_transfers) + .await + .map_err(TransferError::IO)?; + + Ok(resp) +} + +pub async fn transfer_asset( + sk: &str, + request: RgbTransferRequest, +) -> Result { + let (mut stock, mut rgb_account, mut rgb_transfers) = retrieve_stock_account_transfers(sk) + .await + .map_err(TransferError::IO)?; + + let resp = + internal_transfer_asset(request, &mut stock, &mut rgb_account, &mut rgb_transfers).await?; + + store_stock_account_transfers(sk, stock, rgb_account, rgb_transfers) + .await + .map_err(TransferError::IO)?; + + Ok(resp) +} + +async fn internal_transfer_asset( + request: RgbTransferRequest, + stock: &mut Stock, + rgb_account: &mut RgbAccount, + rgb_transfers: &mut RgbTransfers, +) -> 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)); + } + + if rgb_account.wallets.get(RGB_DEFAULT_NAME).is_none() { return Err(TransferError::NoWatcher); } @@ -685,26 +683,16 @@ pub async fn transfer_asset( terminal, } = request; let (psbt, transfer) = - pay_invoice(rgb_invoice.clone(), psbt, &mut stock).map_err(TransferError::Pay)?; + pay_invoice(rgb_invoice.clone(), psbt, stock).map_err(TransferError::Pay)?; - let commit = extract_commit(psbt.clone()).map_err(TransferError::Commitment)?; - let wallet = rgb_account.wallets.get("default"); - if let Some(wallet) = wallet { + let (outpoint, commit) = extract_commit(psbt.clone()).map_err(TransferError::Commitment)?; + if let Some(wallet) = rgb_account.wallets.get(RGB_DEFAULT_NAME) { let mut wallet = wallet.to_owned(); - save_commit(&terminal, commit.clone(), &mut wallet); + save_commit(outpoint, commit.clone(), &terminal, &mut wallet); rgb_account .wallets - .insert("default".to_string(), wallet.clone()); - - store_wallets(sk, ASSETS_WALLETS, &rgb_account) - .await - .map_err(|_| { - TransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.to_string(), - ) - })?; + .insert(RGB_DEFAULT_NAME.to_string(), wallet.clone()); }; let consig = transfer @@ -751,371 +739,9 @@ pub async fn transfer_asset( commit, }; - store_stock(sk, ASSETS_STOCK, &stock).await.map_err(|_| { - TransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - - store_transfers(sk, ASSETS_TRANSFERS, &rgb_transfers) - .await - .map_err(|_| { - TransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - Ok(resp) } -pub async fn full_transfer_asset( - sk: &str, - request: FullRgbTransferRequest, -) -> 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)); - } - - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await.map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - - let rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await.map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.to_string(), - ) - })?; - - if rgb_account.wallets.get("default").is_none() { - return Err(TransferError::NoWatcher); - } - - let wallet = Some(rgb_account.wallets.get("default").unwrap().to_owned()); - 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()); - TransferError::Validation(errors) - })?; - - let invoice = RgbInvoice::from_str(&request.rgb_invoice).map_err(|_| { - let mut errors = BTreeMap::new(); - errors.insert( - "rgb_invoice".to_string(), - "invalid rgb invoice data".to_string(), - ); - TransferError::Validation(errors) - })?; - - let mut resolver = ExplorerResolver { - explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), - ..Default::default() - }; - - if let TypedState::Amount(target_amount) = invoice.owned_state { - let FullRgbTransferRequest { - contract_id: _, - iface: iface_name, - rgb_invoice, - descriptor, - change_terminal, - fee, - 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, - ] { - 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 mut wallet = wallet.unwrap(); - 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(|_| TransferError::NoIface)?; - let contract_iface = stock - .contract_iface(contract_id, iface.iface_id()) - .map_err(|_| TransferError::NoContract)?; - - let contract_index = contract_index as u32; - sync_wallet(contract_index, &mut wallet, &mut resolver); - prefetch_resolver_utxos( - contract_index, - &mut wallet, - &mut resolver, - Some(RGB_DEFAULT_FETCH_LIMIT), - ) - .await; - prefetch_resolver_allocations(contract_iface, &mut resolver).await; - - let contract = export_contract( - contract_id, - &mut stock, - &mut resolver, - &mut Some(wallet.clone()), - ) - .map_err(TransferError::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("rgb_invoice".to_string(), "insufficient state".to_string()); - return Err(TransferError::Validation(errors)); - } - - let asset_unspent_utxos = &mut next_utxos(contract_index, wallet.clone(), &mut resolver) - .map_err(|_| { - TransferError::Retrive( - "Esplora".to_string(), - "Retrieve Unspent UTXO unavaliable".to_string(), - ) - })?; - - let mut asset_total = 0; - let mut asset_inputs = vec![]; - let mut rng = StdRng::seed_from_u64(1); - let rnd_amount = rng.gen_range(600..1500); - - let mut total_asset_bitcoin_unspend: u64 = 0; - let RgbDescr::Tapret(tapret_desc) = wallet.descr.clone(); - for alloc in allocations.into_iter() { - let mut tapret = none!(); - let mut terminal_indexes = alloc.derivation.split('/'); - if let (_, Some(app), Some(index), _) = ( - terminal_indexes.next(), - terminal_indexes.next(), - terminal_indexes.next(), - terminal_indexes.next(), - ) { - let app = u32::from_str(app).expect("invalid terminal app"); - let index = u32::from_str(index).expect("invalid terminal index"); - let derive_infos = tapret_desc.derive(app, 0..index); - if let Some((d, _)) = derive_infos - .into_iter() - .find(|(d, _)| d.terminal == TerminalPath { app, index } && d.tweak.is_some()) - { - if let Some(tweak) = d.tweak { - tapret = Some(tweak.to_string()); - } - } - } - - match alloc.value { - AllocationValue::Value(alloc_value) => { - if asset_total >= target_amount { - break; - } - - let input = PsbtInputRequest { - descriptor: SecretString(universal_desc.clone()), - utxo: alloc.utxo.clone(), - utxo_terminal: alloc.derivation, - tapret, - }; - if !asset_inputs - .clone() - .into_iter() - .any(|x: PsbtInputRequest| x.utxo == alloc.utxo) - { - asset_inputs.push(input); - 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: SecretString(universal_desc.clone()), - utxo: alloc.utxo.clone(), - utxo_terminal: alloc.derivation, - tapret, - }; - if !asset_inputs - .clone() - .into_iter() - .any(|x| x.utxo == alloc.utxo) - { - asset_inputs.push(input); - 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![]; - if let PsbtFeeRequest::Value(fee_amount) = fee { - let bitcoin_indexes = [0, 1]; - for bitcoin_index in bitcoin_indexes { - sync_wallet(bitcoin_index, &mut wallet, &mut resolver); - prefetch_resolver_utxos( - bitcoin_index, - &mut wallet, - &mut resolver, - Some(BITCOIN_DEFAULT_FETCH_LIMIT), - ) - .await; - prefetch_resolver_user_utxo_status( - bitcoin_index, - &mut wallet, - &mut resolver, - false, - ) - .await; - - let mut unspent_utxos = next_utxos(bitcoin_index, wallet.clone(), &mut resolver) - .map_err(|_| { - TransferError::Retrive( - "Esplora".to_string(), - "Retrieve Unspent UTXO unavaliable".to_string(), - ) - })?; - - all_unspents.append(&mut unspent_utxos); - } - - let mut bitcoin_total = total_asset_bitcoin_unspend; - for utxo in all_unspents { - if bitcoin_total > (fee_amount + rnd_amount) { - break; - } else { - bitcoin_total += utxo.amount; - - let TerminalPath { app, index } = utxo.derivation.terminal; - let btc_input = PsbtInputRequest { - descriptor: SecretString(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); - } - } - } - if bitcoin_total < (fee_amount + rnd_amount + total_bitcoin_spend) { - let mut errors = BTreeMap::new(); - errors.insert("bitcoin".to_string(), "insufficient satoshis".to_string()); - return Err(TransferError::Validation(errors)); - } else { - let network = NETWORK.read().await.to_string(); - let network = Network::from_str(&network) - .map_err(|err| TransferError::WrongNetwork(err.to_string()))?; - - let network = AddressNetwork::from(network); - - let change_address = get_address(1, 1, wallet, network) - .map_err(|err| TransferError::WrongNetwork(err.to_string()))? - .address; - - let change_amount = bitcoin_total - (rnd_amount + fee_amount + total_bitcoin_spend); - let change_bitcoin = format!("{change_address}:{change_amount}"); - bitcoin_changes.push(change_bitcoin); - } - } - - let psbt_req = PsbtRequest { - asset_inputs, - bitcoin_inputs, - bitcoin_changes, - fee, - asset_descriptor_change: None, - asset_terminal_change: Some(change_terminal), - }; - - let psbt_response = create_psbt(sk, psbt_req).await?; - transfer_asset( - sk, - RgbTransferRequest { - rgb_invoice, - psbt: psbt_response.psbt, - terminal: psbt_response.terminal, - }, - ) - .await - } else { - let mut errors = BTreeMap::new(); - errors.insert( - "rgb_invoice".to_string(), - "invalid rgb invoice data".to_string(), - ); - Err(TransferError::Validation(errors)) - } -} - pub async fn accept_transfer( sk: &str, request: AcceptRequest, @@ -1128,19 +754,13 @@ pub async fn accept_transfer( .collect(); return Err(TransferError::Validation(errors)); } - - let AcceptRequest { consignment, force } = request; - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await.map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - + let mut stock = retrieve_rgb_stock(sk).await.map_err(TransferError::IO)?; let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() }; + + let AcceptRequest { consignment, force } = request; prefetch_resolver_rgb(&consignment, &mut resolver, None).await; let transfer = accept_rgb_transfer(consignment, force, &mut resolver, &mut stock) @@ -1152,12 +772,9 @@ pub async fn accept_transfer( valid: true, }; - store_stock(sk, ASSETS_STOCK, &stock).await.map_err(|_| { - TransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; + store_rgb_stock(sk, stock) + .await + .map_err(TransferError::IO)?; Ok(resp) } @@ -1167,8 +784,8 @@ pub async fn accept_transfer( pub enum SaveTransferError { /// Some request data is missing. {0:?} Validation(BTreeMap), - /// Retrieve I/O or connectivity error. {1} in {0} - Retrive(String, String), + /// I/O or connectivity error. {0} + IO(RgbPersistenceError), /// Occurs an error in parse consig step. {0} WrongConsig(AcceptTransferError), /// Write I/O or connectivity error. {1} in {0} @@ -1194,14 +811,9 @@ pub async fn save_transfer( consignment, } = request; - let mut rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) + let mut rgb_transfers = retrieve_transfers(sk) .await - .map_err(|_| { - SaveTransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - TRANSFER_UNAVAILABLE.to_string(), - ) - })?; + .map_err(SaveTransferError::IO)?; let (txid, transfer) = extract_transfer(contract_id.clone(), consignment) .map_err(SaveTransferError::WrongConsig)?; @@ -1232,14 +844,9 @@ pub async fn save_transfer( .insert(contract_id.clone(), vec![rgb_transfer]); } - store_transfers(sk, ASSETS_TRANSFERS, &rgb_transfers) + store_transfers(sk, rgb_transfers) .await - .map_err(|_| { - SaveTransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; + .map_err(SaveTransferError::IO)?; let mut status = BTreeMap::new(); status.insert(consig_id, false); @@ -1268,14 +875,9 @@ pub async fn remove_transfer( consig_ids, } = request; - let mut rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) + let mut rgb_transfers = retrieve_transfers(sk) .await - .map_err(|_| { - SaveTransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - TRANSFER_UNAVAILABLE.to_string(), - ) - })?; + .map_err(SaveTransferError::IO)?; if let Some(transfers) = rgb_transfers.transfers.get(&contract_id.clone()) { let current_transfers = transfers @@ -1289,14 +891,9 @@ pub async fn remove_transfer( .insert(contract_id.clone(), current_transfers); } - store_transfers(sk, ASSETS_TRANSFERS, &rgb_transfers) + store_transfers(sk, rgb_transfers) .await - .map_err(|_| { - SaveTransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; + .map_err(SaveTransferError::IO)?; let status = consig_ids.into_iter().map(|x| (x, true)).collect(); Ok(RgbTransferStatusResponse { @@ -1305,15 +902,98 @@ pub async fn remove_transfer( }) } -pub async fn get_contract(sk: &str, contract_id: &str) -> Result { - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await?; - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; +pub async fn verify_transfers(sk: &str) -> Result { + let (mut stock, rgb_transfers) = retrieve_stock_transfers(sk) + .await + .map_err(TransferError::IO)?; let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() }; + let mut transfers = vec![]; + let mut rgb_pending = RgbTransfers::default(); + for (contract_id, transfer_activities) in rgb_transfers.transfers { + let mut pending_transfers = vec![]; + let txids: Vec = transfer_activities + .clone() + .into_iter() + .map(|x| Txid::from_str(&x.tx.to_hex()).expect("invalid tx id")) + .collect(); + prefetch_resolver_txs_status(txids, &mut resolver).await; + + for activity in transfer_activities { + let iface = activity.iface.clone(); + let txid = Txid::from_str(&activity.tx.to_hex()).expect("invalid tx id"); + let status = resolver + .txs_status + .get(&txid) + .unwrap_or(&TxStatus::NotFound) + .to_owned(); + + let accept_status = match status.clone() { + TxStatus::Block(_) => { + prefetch_resolver_rgb(&activity.consig, &mut resolver, None).await; + accept_rgb_transfer(activity.consig.clone(), false, &mut resolver, &mut stock) + .map_err(TransferError::Accept)? + } + _ => { + pending_transfers.push(activity.to_owned()); + transfers.push(BatchRgbTransferItem { + iface, + status, + is_accept: false, + contract_id: contract_id.clone(), + consig_id: activity.consig_id.to_string(), + }); + continue; + } + }; + let transfer_id = accept_status.transfer_id(); + let accept_status = accept_status.unbindle(); + if let Some(rgb_status) = accept_status.into_validation_status() { + if rgb_status.validity() == Validity::Valid { + transfers.push(BatchRgbTransferItem { + iface, + status, + is_accept: true, + contract_id: contract_id.clone(), + consig_id: transfer_id.to_string(), + }); + } else { + transfers.push(BatchRgbTransferItem { + iface, + status, + is_accept: false, + contract_id: contract_id.clone(), + consig_id: transfer_id.to_string(), + }); + pending_transfers.push(activity.to_owned()); + } + } + } + + rgb_pending + .transfers + .insert(contract_id.to_string(), pending_transfers); + } + + store_stock_transfers(sk, stock, rgb_pending) + .await + .map_err(TransferError::IO)?; + + Ok(BatchRgbTransferResponse { transfers }) +} + +pub async fn get_contract(sk: &str, contract_id: &str) -> Result { + 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?; + let contract_id = ContractId::from_str(contract_id)?; let wallet = rgb_account.wallets.get("default"); let mut wallet = match wallet { @@ -1329,6 +1009,7 @@ pub async fn get_contract(sk: &str, contract_id: &str) -> Result Result Result Result { - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await?; - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; - - // Prefetch 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?; + let wallet = rgb_account.wallets.get("default"); let mut wallet = match wallet { Some(wallet) => { @@ -1415,14 +1093,14 @@ pub async fn list_contracts(sk: &str) -> Result { rgb_account .wallets .insert(RGB_DEFAULT_NAME.to_string(), wallet); - store_wallets(sk, ASSETS_WALLETS, &rgb_account).await?; + store_account(sk, rgb_account).await?; }; Ok(ContractsResponse { contracts }) } pub async fn list_interfaces(sk: &str) -> Result { - let stock = retrieve_stock(sk, ASSETS_STOCK).await?; + let stock = retrieve_rgb_stock(sk).await?; let mut interfaces = vec![]; for schema_id in stock.schema_ids()? { @@ -1443,7 +1121,7 @@ pub async fn list_interfaces(sk: &str) -> Result { } pub async fn list_schemas(sk: &str) -> Result { - let stock = retrieve_stock(sk, ASSETS_STOCK).await?; + let stock = retrieve_rgb_stock(sk).await?; let mut schemas = vec![]; for schema_id in stock.schema_ids()? { @@ -1463,7 +1141,7 @@ pub async fn list_schemas(sk: &str) -> Result { } pub async fn list_transfers(sk: &str, contract_id: String) -> Result { - let rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS).await?; + let rgb_transfers = retrieve_transfers(sk).await?; let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), @@ -1506,111 +1184,13 @@ pub async fn list_transfers(sk: &str, contract_id: String) -> Result Result { - let rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) - .await - .map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - TRANSFER_UNAVAILABLE.to_string(), - ) - })?; - - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await.map_err(|_| { - TransferError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - let mut resolver = ExplorerResolver { - explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), - ..Default::default() - }; - - let mut transfers = vec![]; - let mut rgb_pending = RgbTransfers::default(); - - for (contract_id, transfer_activities) in rgb_transfers.transfers.clone() { - let mut pending_transfers = vec![]; - let txids: Vec = transfer_activities - .clone() - .into_iter() - .map(|x| Txid::from_str(&x.tx.to_hex()).expect("invalid tx id")) - .collect(); - prefetch_resolver_txs_status(txids, &mut resolver).await; - - for activity in transfer_activities.iter() { - let iface = activity.iface.clone(); - let txid = Txid::from_str(&activity.tx.to_hex()).expect("invalid tx id"); - let status = resolver - .txs_status - .get(&txid) - .unwrap_or(&TxStatus::NotFound) - .to_owned(); - - let accept_status = match status.clone() { - TxStatus::Block(_) => { - prefetch_resolver_rgb(&activity.consig, &mut resolver, None).await; - accept_rgb_transfer(activity.consig.clone(), false, &mut resolver, &mut stock) - .map_err(TransferError::Accept)? - } - _ => continue, - }; - let accept_status = accept_status.unbindle(); - if let Some(rgb_status) = accept_status.validation_status() { - let consig_id = accept_status.transfer_id().to_string(); - transfers.push(if rgb_status.validity() == Validity::Valid { - BatchRgbTransferItem { - iface, - contract_id: contract_id.clone(), - consig_id: consig_id.to_string(), - status, - is_accept: true, - } - } else { - pending_transfers.push(activity.to_owned()); - BatchRgbTransferItem { - iface, - contract_id: contract_id.clone(), - consig_id: consig_id.to_string(), - status, - is_accept: false, - } - }); - } - } - - rgb_pending.transfers.insert(contract_id, pending_transfers); - } - - store_stock(sk, ASSETS_STOCK, &stock).await.map_err(|_| { - TransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - - store_transfers(sk, ASSETS_TRANSFERS, &rgb_pending) - .await - .map_err(|_| { - TransferError::Write( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - - Ok(BatchRgbTransferResponse { transfers }) -} - #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum ImportError { /// Some request data is missing. {0} Validation(String), - /// Retrieve I/O or connectivity error. {1} in {0} - Retrive(String, String), - /// Write I/O or connectivity error. {1} in {0} - Write(String, String), + /// I/O or connectivity error. {0} + IO(RgbPersistenceError), /// Watcher is required for this operation. Watcher, /// Occurs an error in import step. {0} @@ -1620,25 +1200,14 @@ pub enum ImportError { } pub async fn import(sk: &str, request: ImportRequest) -> Result { - let ImportRequest { data, import } = request; - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await.map_err(|_| { - ImportError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - STOCK_UNAVAILABLE.to_string(), - ) - })?; - - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await.map_err(|_| { - ImportError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.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(ImportError::IO)?; + + let ImportRequest { data, import } = request; prefetch_resolver_import_rgb(&data, import.clone(), &mut resolver).await; let wallet = rgb_account.wallets.get("default"); @@ -1667,27 +1236,16 @@ pub async fn import(sk: &str, request: ImportRequest) -> Result Result Result { let WatcherRequest { name, xpub, force } = request; - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await.map_err(|_| { - WatcherError::Retrive( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.to_string(), - ) - })?; + let mut rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; if rgb_account.wallets.contains_key(&name) && force { rgb_account.wallets.remove(&name); @@ -1755,42 +1308,38 @@ pub async fn create_watcher( } } - store_wallets(sk, ASSETS_WALLETS, &rgb_account) + store_account(sk, rgb_account) .await - .map_err(|_| { - WatcherError::Write( - CARBONADO_UNAVAILABLE.to_string(), - WALLET_UNAVAILABLE.to_string(), - ) - })?; + .map_err(WatcherError::IO)?; + Ok(WatcherResponse { name, migrate }) } -pub async fn clear_watcher(sk: &str, name: &str) -> Result { - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; +pub async fn clear_watcher(sk: &str, name: &str) -> Result { + let mut rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; if rgb_account.wallets.contains_key(name) { rgb_account.wallets.remove(name); } - store_wallets(sk, ASSETS_WALLETS, &rgb_account).await?; + store_account(sk, rgb_account) + .await + .map_err(WatcherError::IO)?; Ok(WatcherResponse { name: name.to_string(), migrate: false, }) } -pub async fn watcher_details(sk: &str, name: &str) -> Result { - let mut stock = retrieve_stock(sk, ASSETS_STOCK).await?; - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; +pub async fn watcher_details(sk: &str, name: &str) -> Result { + let (mut stock, mut rgb_account) = + retrieve_stock_account(sk).await.map_err(WatcherError::IO)?; - let wallet = match rgb_account.wallets.get(name) { - Some(wallet) => Ok(wallet.to_owned()), - _ => Err(anyhow!("Wallet watcher not found")), + let mut wallet = match rgb_account.wallets.get(name) { + Some(wallet) => wallet.to_owned(), + _ => return Err(WatcherError::NoWatcher), }; - let mut wallet = wallet?; - // Prefetch let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() @@ -1807,24 +1356,30 @@ pub async fn watcher_details(sk: &str, name: &str) -> Result Result { - let rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; +pub async fn watcher_address( + sk: &str, + name: &str, + address: &str, +) -> Result { + let rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; let mut resp = WatcherUtxoResponse::default(); if let Some(wallet) = rgb_account.wallets.get(name) { @@ -1834,35 +1389,40 @@ pub async fn watcher_address(sk: &str, name: &str, address: &str) -> Result = [0, 1, 9, 20, 21].to_vec(); + let asset_indexes: Vec = [0, 1, 9, 10, 20, 21].to_vec(); let mut wallet = wallet.to_owned(); prefetch_resolver_waddress(address, &mut wallet, &mut resolver, Some(20)).await; - resp.utxos = - register_address(address, asset_indexes, &mut wallet, &mut resolver, Some(20))? - .into_iter() - .map(|utxo| utxo.outpoint.to_string()) - .collect(); + resp.utxos = register_address(address, asset_indexes, &mut wallet, &mut resolver, Some(20)) + .map_err(|op| WatcherError::Validation(op.to_string()))? + .into_iter() + .map(|utxo| utxo.outpoint.to_string()) + .collect(); }; Ok(resp) } -pub async fn watcher_utxo(sk: &str, name: &str, utxo: &str) -> Result { - let rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; +pub async fn watcher_utxo( + sk: &str, + name: &str, + utxo: &str, +) -> Result { + let rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; let mut resp = WatcherUtxoResponse::default(); if let Some(wallet) = rgb_account.wallets.get(name) { - // Prefetch + let network = NETWORK.read().await.to_string(); + let network = + Network::from_str(&network).map_err(|op| WatcherError::Validation(op.to_string()))?; + let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() }; - let network = NETWORK.read().await.to_string(); - let network = Network::from_str(&network)?; - let network = AddressNetwork::from(network); - let asset_indexes: Vec = [0, 1, 9, 20, 21].to_vec(); + let network = AddressNetwork::from(network); + let asset_indexes: Vec = [0, 1, 9, 10, 20, 21].to_vec(); let mut wallet = wallet.to_owned(); prefetch_resolver_wutxo(utxo, network, &mut wallet, &mut resolver, Some(20)).await; @@ -1873,7 +1433,8 @@ pub async fn watcher_utxo(sk: &str, name: &str, utxo: &str) -> Result Result { - let rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; +) -> Result { + let rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; let network = NETWORK.read().await.to_string(); - let network = Network::from_str(&network)?; + let network = + Network::from_str(&network).map_err(|op| WatcherError::Validation(op.to_string()))?; let network = AddressNetwork::from(network); let wallet = match rgb_account.wallets.get(name) { - Some(wallet) => Ok(wallet.to_owned()), - _ => Err(anyhow!("Wallet watcher not found")), + Some(wallet) => wallet.to_owned(), + _ => return Err(WatcherError::NoWatcher), }; let iface_index = match iface { @@ -1904,8 +1466,8 @@ pub async fn watcher_next_address( _ => 10, }; - let wallet = wallet?; - let next_address = next_address(iface_index, wallet, network)?; + let next_address = next_address(iface_index, wallet, network) + .map_err(|op| WatcherError::Validation(op.to_string()))?; let resp = NextAddressResponse { address: next_address.address.to_string(), @@ -1914,22 +1476,23 @@ pub async fn watcher_next_address( Ok(resp) } -pub async fn watcher_next_utxo(sk: &str, name: &str, iface: &str) -> Result { - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; - let wallet = match rgb_account.wallets.get(name) { - Some(wallet) => Ok(wallet.to_owned()), - _ => Err(anyhow!("Wallet watcher not found")), - }; - +pub async fn watcher_next_utxo( + sk: &str, + name: &str, + iface: &str, +) -> Result { + let mut rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; let iface_index = match iface { "RGB20" => 20, "RGB21" => 21, _ => 10, }; - let mut wallet = wallet?; + let mut wallet = match rgb_account.wallets.get(name) { + Some(wallet) => wallet.to_owned(), + _ => return Err(WatcherError::NoWatcher), + }; - // Prefetch let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() @@ -1943,9 +1506,11 @@ pub async fn watcher_next_utxo(sk: &str, name: &str, iface: &str) -> Result Some(UtxoResponse::with( next_utxo.outpoint, next_utxo.amount, @@ -1957,16 +1522,23 @@ pub async fn watcher_next_utxo(sk: &str, name: &str, iface: &str) -> Result Result { - let mut rgb_account = retrieve_wallets(sk, ASSETS_WALLETS).await?; - let wallet = match rgb_account.wallets.get(name) { - Some(wallet) => Ok(wallet.to_owned()), - _ => Err(anyhow!("Wallet watcher not found")), +pub async fn watcher_unspent_utxos( + sk: &str, + name: &str, + iface: &str, +) -> Result { + let mut rgb_account = retrieve_account(sk).await.map_err(WatcherError::IO)?; + let mut wallet = match rgb_account.wallets.get(name) { + Some(wallet) => wallet.to_owned(), + _ => return Err(WatcherError::NoWatcher), }; let iface_index = match iface { @@ -1975,9 +1547,6 @@ pub async fn watcher_unspent_utxos(sk: &str, name: &str, iface: &str) -> Result< _ => 9, }; - let mut wallet = wallet?; - - // Prefetch let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() @@ -1991,9 +1560,10 @@ pub async fn watcher_unspent_utxos(sk: &str, name: &str, iface: &str) -> Result< ) .await; prefetch_resolver_user_utxo_status(iface_index, &mut wallet, &mut resolver, true).await; - sync_wallet(iface_index, &mut wallet, &mut resolver); - let utxos: HashSet = next_utxos(iface_index, wallet.clone(), &mut resolver)? + + let utxos: HashSet = next_utxos(iface_index, wallet.clone(), &mut resolver) + .map_err(|op| WatcherError::Validation(op.to_string()))? .into_iter() .map(|x| UtxoResponse::with(x.outpoint, x.amount, x.status)) .collect(); @@ -2001,7 +1571,10 @@ pub async fn watcher_unspent_utxos(sk: &str, name: &str, iface: &str) -> Result< rgb_account .wallets .insert(RGB_DEFAULT_NAME.to_string(), wallet); - store_wallets(sk, ASSETS_WALLETS, &rgb_account).await?; + + store_account(sk, rgb_account) + .await + .map_err(WatcherError::IO)?; Ok(NextUtxosResponse { utxos: utxos.into_iter().collect(), @@ -2009,9 +1582,9 @@ pub async fn watcher_unspent_utxos(sk: &str, name: &str, iface: &str) -> Result< } pub async fn clear_stock(sk: &str) { - store_stock(sk, ASSETS_STOCK, &Stock::default()) + store_rgb_stock(sk, Stock::default()) .await - .expect("unable store stock"); + .expect("unable clear stock"); } pub async fn decode_invoice(invoice: String) -> Result { diff --git a/src/rgb/carbonado.rs b/src/rgb/carbonado.rs index 17f59e10..2690d04c 100644 --- a/src/rgb/carbonado.rs +++ b/src/rgb/carbonado.rs @@ -1,9 +1,12 @@ use amplify::confinement::{Confined, U32}; use anyhow::Result; +use autosurgeon::{hydrate, reconcile}; 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::{retrieve, store}, @@ -13,14 +16,32 @@ use crate::{ #[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), + /// Changes '{0}' retrieve causes error. {1} + ChangesRetrieve(String, String), + /// Changes '{0}' write causes error. {1} + ChangesWrite(String, String), + /// Fork '{0}' write causes error. {1} + ForkWrite(String, String), + /// Merge '{0}' write causes error. {1} + MergeWrite(String, String), /// Retrieve '{0}' strict-encoding causes error. {1} - StrictRetrive(String, String), + StrictRetrieve(String, String), /// Write '{0}' strict-encoding causes error. {1} StrictWrite(String, String), + /// Retrieve '{0}' serialize causes error. {1} + SerializeRetrieve(String, String), + /// Write '{0}' serialize causes error. {1} + SerializeWrite(String, String), /// Retrieve '{0}' carbonado causes error. {1} - CarbonadoRetrive(String, String), + CarbonadoRetrieve(String, String), /// Write '{0}' carbonado causes error. {1} CarbonadoWrite(String, String), + /// Reconcile '{0}' causes error. {1} + Reconcile(String, String), } pub async fn store_stock(sk: &str, name: &str, stock: &Stock) -> Result<(), StorageError> { @@ -43,9 +64,35 @@ pub async fn store_stock(sk: &str, name: &str, stock: &Stock) -> Result<(), Stor .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) } -pub async fn force_store_stock(sk: &str, name: &str, stock: &Stock) -> Result<(), StorageError> { - let data = stock - .to_strict_serialized::() +pub async fn store_wallets( + sk: &str, + name: &str, + rgb_wallets: &RgbAccount, +) -> Result<(), StorageError> { + let data = to_allocvec(rgb_wallets) + .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, + false, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) +} + +pub async fn store_transfers( + sk: &str, + name: &str, + rgb_transfers: &RgbTransfers, +) -> Result<(), StorageError> { + let data = to_allocvec(rgb_transfers) .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) @@ -54,7 +101,7 @@ pub async fn force_store_stock(sk: &str, name: &str, stock: &Stock) -> Result<() store( sk, - &hashed_name, + &format!("{hashed_name}.c15"), &data, true, Some(RGB_STRICT_TYPE_VERSION.to_vec()), @@ -70,98 +117,155 @@ pub async fn retrieve_stock(sk: &str, name: &str) -> Result let (data, _) = retrieve(sk, &format!("{hashed_name}.c15"), vec![]) .await - .map_err(|op| StorageError::CarbonadoRetrive(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; if data.is_empty() { Ok(Stock::default()) } else { let confined = Confined::try_from_iter(data) - .map_err(|op| StorageError::StrictRetrive(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; let stock = Stock::from_strict_serialized::(confined) - .map_err(|op| StorageError::StrictRetrive(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; Ok(stock) } } -pub async fn store_wallets( - sk: &str, - name: &str, - rgb_wallets: &RgbAccount, -) -> Result<(), StorageError> { - let data = to_allocvec(rgb_wallets) - .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; - +pub async fn retrieve_wallets(sk: &str, name: &str) -> Result { let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) .to_hex() .to_lowercase(); - store( - sk, - &format!("{hashed_name}.c15"), - &data, - false, - Some(RGB_STRICT_TYPE_VERSION.to_vec()), - ) - .await - .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) + 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(RgbAccount::default()) + } else { + let rgb_wallets = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + Ok(rgb_wallets) + } } -pub async fn retrieve_wallets(sk: &str, name: &str) -> Result { +pub async fn retrieve_transfers(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::CarbonadoRetrive(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; if data.is_empty() { - Ok(RgbAccount::default()) + Ok(RgbTransfers::default()) } else { let rgb_wallets = from_bytes(&data) - .map_err(|op| StorageError::StrictRetrive(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; Ok(rgb_wallets) } } -pub async fn store_transfers( - sk: &str, - name: &str, - rgb_transfers: &RgbTransfers, -) -> Result<(), StorageError> { - let data = to_allocvec(rgb_transfers) - .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; - +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() .to_lowercase(); + let main_name = &format!("{hashed_name}.c15"); + let original_name = &format!("{hashed_name}-diff.c15"); + + // let mut original_version = automerge::AutoCommit::new(); + // let (main_bytes, _) = retrieve(sk, &main_name, vec![]) + // .await + // .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; + + // let original: RgbAccount = from_bytes(&main_bytes) + // .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + + // let raw_data = RawRgbAccount::from(original); + // reconcile(&mut original_version, raw_data) + // .map_err(|op| StorageError::Reconcile(name.to_string(), op.to_string()))?; + + let (original_bytes, _) = retrieve(sk, original_name, vec![]) + .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::StrictRetrieve(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 raw_merged: RawRgbAccount = hydrate(&original_version).unwrap(); + let merged: RgbAccount = RgbAccount::from(raw_merged.clone()); + + let mut latest_version = automerge::AutoCommit::new(); + reconcile(&mut latest_version, raw_merged) + .map_err(|op| StorageError::WriteRetrieve(name.to_string(), op.to_string()))?; + + let data = to_allocvec(&merged) + .map_err(|op| StorageError::StrictWrite(name.to_string(), op.to_string()))?; + store( sk, - &format!("{hashed_name}.c15"), + main_name, &data, - false, + true, Some(RGB_STRICT_TYPE_VERSION.to_vec()), ) .await - .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string())) + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(()) } -pub async fn retrieve_transfers(sk: &str, name: &str) -> Result { +pub async fn retrieve_fork_wallets(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![]) + 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::CarbonadoRetrive(name.to_string(), op.to_string()))?; + .map_err(|op| StorageError::CarbonadoRetrieve(name.to_string(), op.to_string()))?; if data.is_empty() { - Ok(RgbTransfers::default()) + Ok(LocalRgbAccount { + doc: automerge::AutoCommit::new().save(), + rgb_account: RgbAccount::default(), + }) } else { - let rgb_wallets = from_bytes(&data) - .map_err(|op| StorageError::StrictRetrive(name.to_string(), op.to_string()))?; - Ok(rgb_wallets) + let mut original_version = automerge::AutoCommit::new(); + let rgb_account: RgbAccount = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrieve(name.to_string(), op.to_string()))?; + + let raw_rgb_account = RawRgbAccount::from(rgb_account.clone()); + reconcile(&mut original_version, raw_rgb_account.clone()) + .map_err(|op| StorageError::Reconcile(name.to_string(), op.to_string()))?; + + let mut fork_version = original_version.fork(); + let original_version = fork_version.save(); + + store( + sk, + original_name, + &original_version, + true, + Some(RGB_STRICT_TYPE_VERSION.to_vec()), + ) + .await + .map_err(|op| StorageError::CarbonadoWrite(name.to_string(), op.to_string()))?; + + Ok(LocalRgbAccount { + doc: fork_version.save(), + rgb_account, + }) } } diff --git a/src/rgb/crdt.rs b/src/rgb/crdt.rs new file mode 100644 index 00000000..076fb4c2 --- /dev/null +++ b/src/rgb/crdt.rs @@ -0,0 +1,286 @@ +use amplify::hex::ToHex; +use autosurgeon::{Hydrate, Reconcile}; +use bitcoin::OutPoint; +use bitcoin_30::bip32::ExtendedPubKey; +use bp::{dbc::tapret::TapretCommitment, Outpoint}; +use rgb::{DeriveInfo, RgbDescr, RgbWallet, Tapret, TerminalPath, Utxo}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; + +use crate::rgb::structs::RgbAccount; + +#[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] +#[display(doc_comments)] +pub enum RgbMergeError { + /// Invalid Tapret Wallet Format + NoTapret, +} + +#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, Reconcile, Hydrate, Default, Display)] +#[display(doc_comments)] +pub struct RawRgbAccount { + pub wallets: HashMap, +} + +impl From for RawRgbAccount { + fn from(wallet: RgbAccount) -> Self { + Self { + wallets: wallet + .wallets + .into_iter() + .map(|(name, wallet)| (name, RawRgbWallet::from(wallet))) + .collect(), + } + } +} + +impl From for RgbAccount { + fn from(raw_account: RawRgbAccount) -> Self { + Self { + wallets: raw_account + .wallets + .into_iter() + .map(|(name, wallet)| (name, RgbWallet::from(wallet))) + .collect(), + } + } +} + +#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, Reconcile, Hydrate, Default, Display)] +#[display(doc_comments)] +pub struct RawRgbWallet { + pub xpub: String, + pub taprets: BTreeMap>, + pub utxos: Vec, +} + +impl From for RawRgbWallet { + fn from(wallet: RgbWallet) -> Self { + let RgbDescr::Tapret(tapret) = wallet.descr; + + let mut raw_uxtos = vec![]; + let mut raw_taprets = BTreeMap::new(); + for (terminal_path, taprets) in tapret.taprets { + let TerminalPath { app, index } = terminal_path; + raw_taprets.insert( + format!("{app}:{index}"), + taprets.into_iter().map(|tap| tap.to_string()).collect(), + ); + } + + for utxo in wallet.utxos { + raw_uxtos.push(RawUtxo::from(utxo)); + } + + Self { + xpub: tapret.xpub.to_string(), + taprets: raw_taprets, + utxos: raw_uxtos, + } + } +} + +impl From for RgbWallet { + fn from(raw_wallet: RawRgbWallet) -> Self { + let xpub = ExtendedPubKey::from_str(&raw_wallet.xpub).expect("invalid xpub"); + let mut tapret = Tapret { + xpub, + taprets: BTreeMap::new(), + }; + + for (raw_terminal, raw_tweaks) in raw_wallet.taprets { + let mut split = raw_terminal.split(':'); + let app = split.next().unwrap(); + let index = split.next().unwrap(); + + let terminal = TerminalPath { + app: app.parse().expect("invalid terminal path app"), + index: index.parse().expect("invalid terminal path index"), + }; + + let taprets = raw_tweaks + .into_iter() + .map(|tap| TapretCommitment::from_str(&tap).expect("invalid taptweak format")) + .collect(); + + tapret.taprets.insert(terminal, taprets); + } + Self { + descr: RgbDescr::Tapret(tapret), + utxos: raw_wallet.utxos.into_iter().map(Utxo::from).collect(), + } + } +} + +#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, Reconcile, Hydrate, Default, Display)] +#[display(doc_comments)] +pub struct RawUtxo { + pub outpoint: String, + pub block: u32, + pub amount: u64, + // DeriveInfo, + pub terminal: String, + pub tweak: Option, +} + +impl From for Utxo { + fn from(raw_utxo: RawUtxo) -> Self { + let mut split = raw_utxo.terminal.split(':'); + let app = split.next().unwrap(); + let index = split.next().unwrap(); + + let terminal = TerminalPath { + app: app.parse().expect("invalid terminal path app"), + index: index.parse().expect("invalid terminal path index"), + }; + + let mut tweak = none!(); + if let Some(taptweak) = raw_utxo.tweak { + tweak = Some(TapretCommitment::from_str(&taptweak).expect("invalid taptweak format")); + } + + let derive_info = DeriveInfo { terminal, tweak }; + + let outpoint = OutPoint::from_str(&raw_utxo.outpoint).expect("invalid outpoint parse"); + let txid = bp::Txid::from_str(&outpoint.txid.to_hex()).expect("invalid txid"); + + Self { + outpoint: Outpoint::new(txid, outpoint.vout), + status: rgb::MiningStatus::Mempool, + amount: raw_utxo.amount, + derivation: derive_info, + } + } +} + +impl From for RawUtxo { + fn from(utxo: Utxo) -> Self { + let Utxo { + outpoint, + status, + amount, + derivation: + DeriveInfo { + terminal: TerminalPath { app, index }, + tweak, + }, + } = utxo; + + let mut tap_tweak = none!(); + if let Some(tap) = tweak { + tap_tweak = Some(tap.to_string()) + } + + let block = match status { + rgb::MiningStatus::Mempool => 0, + rgb::MiningStatus::Blockchain(block) => block, + }; + + Self { + outpoint: outpoint.to_string(), + block, + amount, + terminal: format!("{app}:{index}"), + tweak: tap_tweak, + } + } +} + +pub trait RgbMerge { + fn update(self, rgb_data: &mut T); +} + +impl RgbMerge for RgbAccount { + fn update(self, rgb_data: &mut RawRgbAccount) { + for (name, wallet) in self.wallets { + if let Some(raw_wallet) = rgb_data.wallets.get(&name) { + let mut raw_wallet = raw_wallet.clone(); + wallet.update(&mut raw_wallet); + rgb_data.wallets.insert(name, raw_wallet); + } else { + rgb_data.wallets.insert(name, RawRgbWallet::from(wallet)); + } + } + } +} + +impl RgbMerge for RgbWallet { + fn update(self, rgb_data: &mut RawRgbWallet) { + let RgbDescr::Tapret(tapret) = self.descr; + + for (terminal_path, taprets) in tapret.taprets { + let TerminalPath { app, index } = terminal_path; + let terminal = format!("{app}:{index}"); + + let mut current_taprets = match rgb_data.taprets.get(&terminal) { + Some(taprets) => taprets.clone(), + None => vec![], + }; + + let new_taprets: Vec = taprets.into_iter().map(|tap| tap.to_string()).collect(); + + for new_tapret in new_taprets { + if !current_taprets.contains(&new_tapret) { + current_taprets.push(new_tapret); + } + } + + rgb_data.taprets.insert(terminal, current_taprets); + } + + for utxo in self.utxos { + let new_utxo = RawUtxo::from(utxo); + if !rgb_data.utxos.contains(&new_utxo) { + rgb_data.utxos.push(new_utxo); + } + } + } +} + +impl RgbMerge for Utxo { + fn update(self, rgb_data: &mut RawUtxo) { + let Utxo { + status, + amount, + derivation: DeriveInfo { tweak, .. }, + .. + } = self; + + if rgb_data.amount != amount { + rgb_data.amount = amount; + } + + if rgb_data.block == 0 { + rgb_data.block = match status { + rgb::MiningStatus::Mempool => 0, + rgb::MiningStatus::Blockchain(block) => block, + }; + } + + let new_tap = if let Some(new_tap) = tweak { + Some(new_tap.to_string()) + } else { + none!() + }; + + if rgb_data.tweak.is_none() { + rgb_data.tweak = new_tap; + }; + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, Display)] +#[display(doc_comments)] +pub struct LocalRgbAccount { + pub doc: Vec, + pub rgb_account: RgbAccount, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct LocalCopyData { + pub doc: Vec, + pub data: Vec, +} diff --git a/src/rgb/fs.rs b/src/rgb/fs.rs new file mode 100644 index 00000000..d3a34fb8 --- /dev/null +++ b/src/rgb/fs.rs @@ -0,0 +1,138 @@ +use rgbstd::persistence::Stock; + +use crate::constants::storage_keys::{ASSETS_STOCK, ASSETS_TRANSFERS, ASSETS_WALLETS}; +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, + }, + crdt::LocalRgbAccount, + structs::{RgbAccount, RgbTransfers}, +}; + +#[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] +#[display(doc_comments)] +pub enum RgbPersistenceError { + // Retrieve Stock Error. {0} + RetrieveStock(String), + // Retrieve RgbAccount Error. {0} + RetrieveRgbAccount(String), + // Retrieve RgbAccount (Fork) Error. {0} + RetrieveRgbAccountFork(String), + // Retrieve Transfers Error. {0} + RetrieveRgbTransfers(String), + // Store Stock Error. {0} + WriteStock(String), + // Store RgbAccount Error. {0} + WriteRgbAccount(String), + // Store RgbAccount (Fork) Error. {0} + WriteRgbAccountFork(String), + // Store Transfers Error. {0} + WriteRgbTransfers(String), +} + +pub async fn retrieve_stock(sk: &str) -> Result { + let stock = retrieve_rgb_stock(sk, ASSETS_STOCK) + .await + .map_err(|op| RgbPersistenceError::RetrieveStock(op.to_string()))?; + + Ok(stock) +} + +pub async fn retrieve_transfers(sk: &str) -> Result { + let rgb_account = retrieve_rgb_transfers(sk, ASSETS_TRANSFERS) + .await + .map_err(|op| RgbPersistenceError::RetrieveRgbTransfers(op.to_string()))?; + + Ok(rgb_account) +} + +pub async fn retrieve_account(sk: &str) -> Result { + let rgb_account = retrieve_wallets(sk, ASSETS_WALLETS) + .await + .map_err(|op| RgbPersistenceError::RetrieveRgbAccount(op.to_string()))?; + + Ok(rgb_account) +} + +pub async fn retrieve_local_account(sk: &str) -> Result { + let rgb_account = retrieve_fork_wallets(sk, ASSETS_WALLETS) + .await + .map_err(|op| RgbPersistenceError::RetrieveRgbAccountFork(op.to_string()))?; + + Ok(rgb_account) +} + +pub async fn retrieve_stock_account(sk: &str) -> Result<(Stock, RgbAccount), RgbPersistenceError> { + Ok((retrieve_stock(sk).await?, retrieve_account(sk).await?)) +} + +pub async fn retrieve_stock_transfers( + sk: &str, +) -> Result<(Stock, RgbTransfers), RgbPersistenceError> { + Ok((retrieve_stock(sk).await?, retrieve_transfers(sk).await?)) +} + +pub async fn retrieve_stock_account_transfers( + sk: &str, +) -> Result<(Stock, RgbAccount, RgbTransfers), RgbPersistenceError> { + Ok(( + retrieve_stock(sk).await?, + retrieve_account(sk).await?, + retrieve_transfers(sk).await?, + )) +} + +pub async fn store_stock(sk: &str, stock: Stock) -> Result<(), RgbPersistenceError> { + store_rgb_stock(sk, ASSETS_STOCK, &stock) + .await + .map_err(|op| RgbPersistenceError::WriteStock(op.to_string())) +} + +pub async fn store_transfers(sk: &str, transfers: RgbTransfers) -> Result<(), RgbPersistenceError> { + store_rgb_transfer(sk, ASSETS_TRANSFERS, &transfers) + .await + .map_err(|op| RgbPersistenceError::WriteRgbTransfers(op.to_string())) +} + +pub async fn store_account(sk: &str, account: RgbAccount) -> Result<(), RgbPersistenceError> { + store_wallets(sk, ASSETS_WALLETS, &account) + .await + .map_err(|op| RgbPersistenceError::WriteRgbAccount(op.to_string())) +} + +pub async fn store_local_account(sk: &str, changes: Vec) -> Result<(), RgbPersistenceError> { + store_fork_wallets(sk, ASSETS_WALLETS, &changes) + .await + .map_err(|op| RgbPersistenceError::WriteRgbAccountFork(op.to_string())) +} + +pub async fn store_stock_account( + sk: &str, + stock: Stock, + account: RgbAccount, +) -> Result<(), RgbPersistenceError> { + store_stock(sk, stock).await?; + store_account(sk, account).await +} + +pub async fn store_stock_transfers( + sk: &str, + stock: Stock, + transfers: RgbTransfers, +) -> Result<(), RgbPersistenceError> { + store_stock(sk, stock).await?; + store_transfers(sk, transfers).await +} + +pub async fn store_stock_account_transfers( + sk: &str, + stock: Stock, + account: RgbAccount, + transfers: RgbTransfers, +) -> Result<(), RgbPersistenceError> { + store_stock(sk, stock).await?; + store_account(sk, account).await?; + store_transfers(sk, transfers).await +} diff --git a/src/rgb/prebuild.rs b/src/rgb/prebuild.rs new file mode 100644 index 00000000..99a53a47 --- /dev/null +++ b/src/rgb/prebuild.rs @@ -0,0 +1,304 @@ +use std::{collections::BTreeMap, str::FromStr}; + +use bitcoin::Network; +use bitcoin_scripts::address::AddressNetwork; +use garde::Validate; +use rand::{rngs::StdRng, Rng, SeedableRng}; +use rgb::{RgbWallet, TerminalPath}; +use rgbstd::{ + contract::ContractId, + interface::TypedState, + persistence::{Inventory, Stash, Stock}, +}; +use rgbwallet::RgbInvoice; +use strict_encoding::tn; + +use crate::{ + constants::NETWORK, + structs::{ + AllocationDetail, AllocationValue, AssetType, FullRgbTransferRequest, PsbtFeeRequest, + PsbtInputRequest, SecretString, + }, + validators::RGBContext, +}; + +use crate::rgb::{ + constants::{BITCOIN_DEFAULT_FETCH_LIMIT, RGB_DEFAULT_FETCH_LIMIT}, + contract::export_contract, + fs::RgbPersistenceError, + prefetch::{ + prefetch_resolver_allocations, prefetch_resolver_user_utxo_status, prefetch_resolver_utxos, + }, + resolvers::ExplorerResolver, + structs::AddressAmount, + wallet::sync_wallet, + wallet::{get_address, next_utxos}, + TransferError, +}; + +pub async fn prebuild_transfer_asset( + request: FullRgbTransferRequest, + stock: &mut Stock, + rgb_wallet: &mut RgbWallet, + resolver: &mut ExplorerResolver, +) -> Result<(Vec, Vec, Vec), TransferError> { + 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)); + } + + 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()); + TransferError::Validation(errors) + })?; + + let invoice = RgbInvoice::from_str(&request.rgb_invoice).map_err(|_| { + let mut errors = BTreeMap::new(); + errors.insert( + "rgb_invoice".to_string(), + "invalid rgb invoice data".to_string(), + ); + TransferError::Validation(errors) + })?; + + let target_amount = match invoice.owned_state { + TypedState::Amount(target_amount) => target_amount, + _ => { + let mut errors = BTreeMap::new(); + errors.insert( + "rgb_invoice".to_string(), + "invalid rgb invoice data".to_string(), + ); + return Err(TransferError::Validation(errors)); + } + }; + + let FullRgbTransferRequest { + contract_id: _, + iface: iface_name, + rgb_invoice: _, + descriptor, + change_terminal: _, + fee, + 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, + ] { + 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 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(|_| TransferError::NoIface)?; + let contract_iface = stock + .contract_iface(contract_id, iface.iface_id()) + .map_err(|_| TransferError::NoContract)?; + + let contract_index = contract_index as u32; + prefetch_resolver_allocations(contract_iface, resolver).await; + sync_wallet(contract_index, rgb_wallet, resolver); + prefetch_resolver_utxos( + contract_index, + rgb_wallet, + resolver, + Some(RGB_DEFAULT_FETCH_LIMIT), + ) + .await; + + let contract = export_contract(contract_id, stock, resolver, &mut Some(rgb_wallet.clone())) + .map_err(TransferError::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("rgb_invoice".to_string(), "insufficient state".to_string()); + return Err(TransferError::Validation(errors)); + } + + let asset_unspent_utxos = &mut next_utxos(contract_index, rgb_wallet.clone(), resolver) + .map_err(|_| TransferError::IO(RgbPersistenceError::RetrieveRgbAccount("".to_string())))?; + + let mut asset_total = 0; + let mut asset_inputs = vec![]; + let mut rng = StdRng::seed_from_u64(1); + let rnd_amount = rng.gen_range(600..1500); + + let mut total_asset_bitcoin_unspend: u64 = 0; + for alloc in allocations.into_iter() { + match alloc.value { + AllocationValue::Value(alloc_value) => { + if asset_total >= target_amount { + break; + } + + let input = PsbtInputRequest { + descriptor: SecretString(universal_desc.clone()), + utxo: alloc.utxo.clone(), + utxo_terminal: alloc.derivation, + tapret: None, + }; + if !asset_inputs + .clone() + .into_iter() + .any(|x: PsbtInputRequest| x.utxo == alloc.utxo) + { + asset_inputs.push(input); + 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: SecretString(universal_desc.clone()), + utxo: alloc.utxo.clone(), + utxo_terminal: alloc.derivation, + tapret: None, + }; + if !asset_inputs + .clone() + .into_iter() + .any(|x| x.utxo == alloc.utxo) + { + asset_inputs.push(input); + 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![]; + if let PsbtFeeRequest::Value(fee_amount) = fee.clone() { + 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(|_| { + TransferError::IO(RgbPersistenceError::RetrieveRgbAccount("".to_string())) + })?; + + all_unspents.append(&mut unspent_utxos); + } + + let mut bitcoin_total = total_asset_bitcoin_unspend; + for utxo in all_unspents { + if bitcoin_total > (fee_amount + rnd_amount) { + break; + } else { + bitcoin_total += utxo.amount; + + let TerminalPath { app, index } = utxo.derivation.terminal; + let btc_input = PsbtInputRequest { + descriptor: SecretString(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); + } + } + } + if bitcoin_total < (fee_amount + rnd_amount + total_bitcoin_spend) { + let mut errors = BTreeMap::new(); + errors.insert("bitcoin".to_string(), "insufficient satoshis".to_string()); + return Err(TransferError::Validation(errors)); + } else { + let network = NETWORK.read().await.to_string(); + let network = Network::from_str(&network) + .map_err(|err| TransferError::WrongNetwork(err.to_string()))?; + + let network = AddressNetwork::from(network); + + let change_address = get_address(1, 1, rgb_wallet.clone(), network) + .map_err(|err| TransferError::WrongNetwork(err.to_string()))? + .address; + + let change_amount = bitcoin_total - (rnd_amount + fee_amount + total_bitcoin_spend); + let change_bitcoin = format!("{change_address}:{change_amount}"); + bitcoin_changes.push(change_bitcoin); + } + } + + Ok((asset_inputs, bitcoin_inputs, bitcoin_changes)) +} diff --git a/src/rgb/prefetch.rs b/src/rgb/prefetch.rs index 0f184347..6731628e 100644 --- a/src/rgb/prefetch.rs +++ b/src/rgb/prefetch.rs @@ -300,6 +300,7 @@ pub async fn prefetch_resolver_utxos( explorer: &mut ExplorerResolver, limit: Option, ) { + // use crate::debug; use std::collections::HashSet; let esplora_client: EsploraBlockchain = EsploraBlockchain::new(&explorer.explorer_url, 1).with_concurrency(6); @@ -310,8 +311,6 @@ pub async fn prefetch_resolver_utxos( step = limit; } - let mut utxos = bset![]; - let scripts = wallet.descr.derive(iface_index, index..step); let new_scripts: BTreeMap = scripts.into_iter().map(|(d, sc)| (d, sc)).collect(); @@ -327,6 +326,7 @@ pub async fn prefetch_resolver_utxos( .collect::>() .into_iter(); + let mut new_utxos = bset![]; for (derive, script) in script_list { let mut related_txs = esplora_client .scripthash_txs(&script, None) @@ -370,13 +370,30 @@ pub async fn prefetch_resolver_utxos( amount: vout.value, derivation: derive.clone(), }; - utxos.insert(new_utxo); + new_utxos.insert(new_utxo); } }); } - if !utxos.is_empty() { - wallet.utxos.append(&mut utxos); + for mut new_utxo in new_utxos { + if let Some(current_utxo) = wallet + .utxos + .clone() + .into_iter() + .find(|u| u.outpoint == new_utxo.outpoint) + { + if current_utxo.status == MiningStatus::Mempool { + wallet.utxos.remove(¤t_utxo.clone()); + explorer.utxos.insert(current_utxo.clone()); + + new_utxo.derivation = current_utxo.derivation; + wallet.utxos.insert(new_utxo.clone()); + explorer.utxos.insert(new_utxo); + } + } else { + wallet.utxos.insert(new_utxo.clone()); + explorer.utxos.insert(new_utxo); + } } } @@ -414,7 +431,7 @@ pub async fn prefetch_resolver_waddress( let script = ScriptBuf::from_hex(&sc.script_pubkey().to_hex()).expect("invalid script"); let mut scripts: BTreeMap = BTreeMap::new(); - let asset_indexes: Vec = [0, 1, 9, 20, 21].to_vec(); + let asset_indexes: Vec = [0, 1, 9, 10, 20, 21].to_vec(); for app in asset_indexes { scripts.append(&mut wallet.descr.derive(app, index..step)); } @@ -431,7 +448,7 @@ pub async fn prefetch_resolver_waddress( ) }); - let mut utxos = bset![]; + let mut new_utxos = bset![]; for (derive, script) in script_list { let txs = match esplora_client.scripthash_txs(&script, none!()).await { Ok(txs) => txs, @@ -461,13 +478,27 @@ pub async fn prefetch_resolver_waddress( amount: tx.vout[index].value, derivation: derive.clone(), }; - utxos.insert(new_utxo); + new_utxos.insert(new_utxo); } }); } - if !utxos.is_empty() { - wallet.utxos.append(&mut utxos); + for mut new_utxo in new_utxos { + if let Some(current_utxo) = wallet + .utxos + .clone() + .into_iter() + .find(|u| u.outpoint == new_utxo.outpoint) + { + if current_utxo.status == MiningStatus::Mempool { + wallet.utxos.remove(¤t_utxo); + + new_utxo.derivation = current_utxo.derivation; + wallet.utxos.insert(new_utxo); + } + } else { + wallet.utxos.insert(new_utxo); + } } } } diff --git a/src/rgb/psbt.rs b/src/rgb/psbt.rs index 4e797608..0a11e603 100644 --- a/src/rgb/psbt.rs +++ b/src/rgb/psbt.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use amplify::hex::ToHex; +use amplify::hex::{FromHex, ToHex}; use bdk::{ wallet::coin_selection::{decide_change, Excess}, FeeRate, @@ -15,7 +15,7 @@ use bitcoin::{EcdsaSighashType, OutPoint, Script, XOnlyPublicKey}; use bitcoin_30::{secp256k1::SECP256K1 as SECP256K1_30, taproot::TaprootBuilder, ScriptBuf}; use bitcoin_blockchain::locks::SeqNo; use bitcoin_scripts::PubkeyScript; -use bp::{dbc::tapret::TapretCommitment, TapScript}; +use bp::{dbc::tapret::TapretCommitment, Outpoint, TapScript, Vout}; use commit_verify::{mpc::Commitment, CommitVerify}; use miniscript_crate::Descriptor; use psbt::{ProprietaryKey, ProprietaryKeyType}; @@ -24,7 +24,7 @@ use rgb::{ DbcPsbtError, TapretKeyError, PSBT_OUT_TAPRET_COMMITMENT, PSBT_OUT_TAPRET_HOST, PSBT_TAPRET_PREFIX, }, - Resolver, RgbDescr, RgbWallet, TerminalPath, + DeriveInfo, MiningStatus, Resolver, RgbDescr, RgbWallet, TerminalPath, Utxo, }; use wallet::{ descriptors::{derive::DeriveDescriptor, InputDescriptor}, @@ -329,10 +329,10 @@ fn complete_input_desc( } } -pub fn extract_commit(mut psbt: Psbt) -> Result, DbcPsbtError> { - let (_, output) = psbt +pub fn extract_commit(psbt: Psbt) -> Result<(Outpoint, Vec), DbcPsbtError> { + let (index, output) = psbt .outputs - .iter_mut() + .iter() .enumerate() .find(|(_, output)| { output.proprietary.contains_key(&ProprietaryKey { @@ -351,12 +351,16 @@ pub fn extract_commit(mut psbt: Psbt) -> Result, DbcPsbtError> { }); match commit_vec { - Some(commit) => Ok(commit.to_owned()), + Some(commit) => { + let txid = bp::Txid::from_hex(&psbt.to_txid().to_hex()).expect("invalid outpoint"); + let vout = Vout::from_str(&index.to_string()).expect("invalid vout"); + Ok((Outpoint::new(txid, vout), commit.to_owned())) + } _ => Err(DbcPsbtError::TapretKey(TapretKeyError::InvalidProof)), } } -pub fn save_commit(terminal: &str, commit: Vec, wallet: &mut RgbWallet) { +pub fn save_commit(outpoint: Outpoint, commit: Vec, terminal: &str, wallet: &mut RgbWallet) { let descr = wallet.descr.clone(); let RgbDescr::Tapret(mut tapret) = descr; let derive: Vec<&str> = terminal.split('/').filter(|s| !s.is_empty()).collect(); @@ -369,15 +373,22 @@ pub fn save_commit(terminal: &str, commit: Vec, wallet: &mut RgbWallet) { }; let mpc = Commitment::from_str(&commit.to_hex()).expect("invalid tapret"); - let tap_commit = &TapretCommitment::with(mpc, 0); + let tap_commit = TapretCommitment::with(mpc, 0); if let Some(taprets) = tapret.taprets.get(&terminal) { let mut current_taprets = taprets.clone(); current_taprets.insert(tap_commit.clone()); + tapret.taprets.insert(terminal, current_taprets.clone()); } else { tapret.taprets.insert(terminal, bset! {tap_commit.clone()}); } + wallet.utxos.insert(Utxo { + amount: 0, + outpoint, + status: MiningStatus::Mempool, + derivation: DeriveInfo::with(terminal.app, terminal.index, Some(tap_commit.clone())), + }); wallet.descr = RgbDescr::Tapret(tapret); } diff --git a/src/rgb/wallet.rs b/src/rgb/wallet.rs index 3ee45b4b..38aac40c 100644 --- a/src/rgb/wallet.rs +++ b/src/rgb/wallet.rs @@ -11,9 +11,7 @@ use bitcoin_scripts::{ address::{AddressCompat, AddressNetwork}, PubkeyScript, }; -use bp::dbc::tapret::TapretCommitment; -use commit_verify::mpc::Commitment; -use rgb::{DeriveInfo, Resolver, RgbDescr, RgbWallet, SpkDescriptor, Tapret, TerminalPath, Utxo}; +use rgb::{DeriveInfo, MiningStatus, Resolver, RgbDescr, RgbWallet, SpkDescriptor, Tapret, Utxo}; use rgbstd::{ contract::ContractId, persistence::{Inventory, Stash, Stock}, @@ -210,7 +208,7 @@ pub fn next_utxos( let utxo_status = resolver .resolve_spent_status(txid, index.into(), true) .expect("unavaliable service"); - if !utxo_status.is_spent { + if !utxo_status.is_spent && !next_utxo.contains(&utxo) { next_utxo.push(utxo); } } @@ -224,11 +222,26 @@ pub fn sync_wallet(iface_index: u32, wallet: &mut RgbWallet, resolver: &mut impl let scripts = wallet.descr.derive(iface_index, index..step); let new_scripts = scripts.into_iter().map(|(d, sc)| (d, sc)).collect(); - let mut new_utxos = resolver + let new_utxos = resolver .resolve_utxo(new_scripts) .expect("service unavalible"); - if !new_utxos.is_empty() { - wallet.utxos.append(&mut new_utxos); + + for mut new_utxo in new_utxos { + if let Some(current_utxo) = wallet + .utxos + .clone() + .into_iter() + .find(|u| u.outpoint == new_utxo.outpoint) + { + if current_utxo.status == MiningStatus::Mempool { + wallet.utxos.remove(¤t_utxo); + + new_utxo.derivation = current_utxo.derivation; + wallet.utxos.insert(new_utxo); + } + } else { + wallet.utxos.insert(new_utxo); + } } } @@ -301,29 +314,6 @@ where Ok(utxos) } -pub fn save_commitment( - iface_index: u32, - path: TerminalPath, - commit: String, - wallet: &mut RgbWallet, -) { - let mpc = Commitment::from_str(&commit).expect("invalid commitment"); - let tap_commit = TapretCommitment::with(mpc, 0); - - let mut utxo = wallet - .utxos - .clone() - .into_iter() - .find(|utxo| { - utxo.derivation.terminal.app == iface_index && utxo.derivation.terminal == path - }) - .expect("invalid UTXO reference"); - - wallet.utxos.remove(&utxo); - utxo.derivation.tweak = Some(tap_commit); - wallet.utxos.insert(utxo); -} - pub fn list_allocations( wallet: &mut RgbWallet, stock: &mut Stock, diff --git a/src/structs.rs b/src/structs.rs index df276568..d8766069 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -882,8 +882,9 @@ pub struct NextUtxosResponse { pub utxos: Vec, } -#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Debug, Clone, Default, Display)] #[serde(rename_all = "camelCase")] +#[display("{outpoint}:{amount}")] pub struct UtxoResponse { pub outpoint: String, pub amount: u64, diff --git a/src/util.rs b/src/util.rs index 20cc3abf..50b18660 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,7 +7,7 @@ use serde::Serialize; #[macro_export] macro_rules! info { ($($arg:expr),+) => { - let output = vec![$(String::from($arg.to_owned()),)+].join(" "); + let output = [$(String::from($arg.to_owned()),)+].join(" "); #[cfg(target_arch = "wasm32")] gloo_console::info!(format!("{}", output)); #[cfg(not(target_arch = "wasm32"))] @@ -18,7 +18,7 @@ macro_rules! info { #[macro_export] macro_rules! debug { ($($arg:expr),+) => { - let output = vec![$(String::from($arg.to_owned()),)+].join(" "); + let output = [$(String::from($arg.to_owned()),)+].join(" "); #[cfg(target_arch = "wasm32")] gloo_console::debug!(format!("{}", output)); #[cfg(not(target_arch = "wasm32"))] @@ -29,7 +29,7 @@ macro_rules! debug { #[macro_export] macro_rules! error { ($($arg:expr),+) => { - let output = vec![$(String::from($arg.to_owned()),)+].join(" "); + let output = [$(String::from($arg.to_owned()),)+].join(" "); #[cfg(target_arch = "wasm32")] gloo_console::error!(format!("{}", output)); #[cfg(not(target_arch = "wasm32"))] @@ -40,7 +40,7 @@ macro_rules! error { #[macro_export] macro_rules! warn { ($($arg:expr),+) => { - let output = vec![$(String::from($arg.to_owned()),)+].join(" "); + let output = [$(String::from($arg.to_owned()),)+].join(" "); #[cfg(target_arch = "wasm32")] gloo_console::warn!(format!("{}", output)); #[cfg(not(target_arch = "wasm32"))] @@ -51,7 +51,7 @@ macro_rules! warn { #[macro_export] macro_rules! trace { ($($arg:expr),+) => { - let output = vec![$(String::from($arg.to_owned()),)+].join(" "); + let output = [$(String::from($arg.to_owned()),)+].join(" "); #[cfg(target_arch = "wasm32")] gloo_console::trace!(format!("{}", output)); #[cfg(not(target_arch = "wasm32"))] diff --git a/src/web.rs b/src/web.rs index 08824492..6c43820f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -95,6 +95,16 @@ pub mod constants { Ok(JsValue::UNDEFINED) }) } + + #[wasm_bindgen] + pub fn sleep(ms: i32) -> js_sys::Promise { + js_sys::Promise::new(&mut |resolve, _| { + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms) + .unwrap(); + }) + } } pub mod bitcoin { @@ -109,6 +119,20 @@ pub mod bitcoin { .to_owned() } + #[wasm_bindgen] + pub fn new_mnemonic(password: String) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::bitcoin::new_mnemonic(&SecretString(password)).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 decrypt_wallet(hash: String, encrypted_descriptors: String) -> Promise { set_panic_hook(); diff --git a/tests/rgb.rs b/tests/rgb.rs index 7eaeb0cd..db91a967 100644 --- a/tests/rgb.rs +++ b/tests/rgb.rs @@ -12,10 +12,9 @@ mod rgb { mod integration { // TODO: Review after support multi-token transfer // mod collectibles; - mod collectibles; - mod accept; - mod consecutive; + mod collectibles; + mod crdt; mod drain; mod dustless; mod fungibles; diff --git a/tests/rgb/integration/accept.rs b/tests/rgb/integration/accept.rs index 8ab7773e..70fa4c95 100644 --- a/tests/rgb/integration/accept.rs +++ b/tests/rgb/integration/accept.rs @@ -92,7 +92,7 @@ pub async fn allow_save_read_remove_transfers() -> Result<()> { let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -254,7 +254,7 @@ pub async fn allow_save_and_accept_all_transfers() -> Result<()> { let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), diff --git a/tests/rgb/integration/consecutive.rs b/tests/rgb/integration/consecutive.rs deleted file mode 100644 index d81bf427..00000000 --- a/tests/rgb/integration/consecutive.rs +++ /dev/null @@ -1,548 +0,0 @@ -#![cfg(not(target_arch = "wasm32"))] - -use bdk::wallet::AddressIndex; -use bitmask_core::{ - bitcoin::{get_wallet, new_mnemonic, save_mnemonic, sign_psbt_file, sync_wallet}, - rgb::{ - accept_transfer, create_watcher, full_transfer_asset, get_contract, save_transfer, - verify_transfers, - }, - structs::{ - AcceptRequest, FullRgbTransferRequest, IssueResponse, PsbtFeeRequest, - RgbSaveTransferRequest, RgbTransferResponse, SecretString, SignPsbtRequest, WatcherRequest, - }, -}; - -use crate::rgb::integration::utils::{ - create_new_invoice, create_new_invoice_v2, get_uda_data, issuer_issue_contract_v2, - send_some_coins, UtxoFilter, ISSUER_MNEMONIC, OWNER_MNEMONIC, -}; - -#[ignore] -#[tokio::test] -async fn allow_fungible_full_transfer_op() -> anyhow::Result<()> { - // 1. Initial Setup - let issuer_keys = save_mnemonic( - &SecretString(ISSUER_MNEMONIC.to_string()), - &SecretString("".to_string()), - ) - .await?; - let owner_keys = save_mnemonic( - &SecretString(OWNER_MNEMONIC.to_string()), - &SecretString("".to_string()), - ) - .await?; - let issuer_resp = issuer_issue_contract_v2( - 1, - "RGB20", - 1, - false, - true, - None, - Some("0.00000546".to_string()), - Some(UtxoFilter::with_amount_less_than(546)), - None, - ) - .await?; - - // 2. Get Invoice - let issuer_resp = issuer_resp[0].clone(); - let owner_resp = &create_new_invoice( - &issuer_resp.contract_id, - &issuer_resp.iface, - 1, - owner_keys.clone(), - None, - Some(issuer_resp.clone().contract.strict), - ) - .await?; - - // 3. Get Bitcoin UTXO - let issuer_btc_desc = &issuer_keys.public.btc_change_descriptor_xpub; - let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; - let issuer_address = &issuer_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - send_some_coins(issuer_address, "0.001").await; - sync_wallet(&issuer_vault).await?; - - // 4. Make a Self Payment - let self_pay_req = FullRgbTransferRequest { - contract_id: issuer_resp.contract_id, - iface: issuer_resp.iface, - rgb_invoice: owner_resp.invoice.to_string(), - descriptor: SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(1000), - bitcoin_changes: vec![], - }; - - let issue_sk = issuer_keys.private.nostr_prv.to_string(); - let resp = full_transfer_asset(&issue_sk, self_pay_req).await; - - assert!(resp.is_ok()); - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn allow_uda_full_transfer_op() -> anyhow::Result<()> { - // 1. Initial Setup - let issuer_keys = save_mnemonic( - &SecretString(ISSUER_MNEMONIC.to_string()), - &SecretString("".to_string()), - ) - .await?; - let owner_keys = save_mnemonic( - &SecretString(OWNER_MNEMONIC.to_string()), - &SecretString("".to_string()), - ) - .await?; - let meta = Some(get_uda_data()); - let issuer_resp = issuer_issue_contract_v2( - 1, - "RGB21", - 1, - false, - true, - meta, - Some("0.00000546".to_string()), - Some(UtxoFilter::with_amount_less_than(546)), - None, - ) - .await?; - - // 2. Get Invoice - let issuer_resp = issuer_resp[0].clone(); - let owner_resp = &create_new_invoice( - &issuer_resp.contract_id, - &issuer_resp.iface, - 1, - owner_keys.clone(), - None, - None, - ) - .await?; - - // 3. Get Bitcoin UTXO - let issuer_btc_desc = &issuer_keys.public.btc_change_descriptor_xpub; - let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; - let issuer_address = &issuer_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - send_some_coins(issuer_address, "0.001").await; - sync_wallet(&issuer_vault).await?; - - // 4. Make a Self Payment - let self_pay_req = FullRgbTransferRequest { - contract_id: issuer_resp.contract_id, - iface: issuer_resp.iface, - rgb_invoice: owner_resp.invoice.to_string(), - descriptor: SecretString(issuer_keys.public.rgb_udas_descriptor_xpub.to_string()), - change_terminal: "/21/1".to_string(), - fee: PsbtFeeRequest::Value(546), - bitcoin_changes: vec![], - }; - - let issue_sk = issuer_keys.private.nostr_prv.to_string(); - let resp = full_transfer_asset(&issue_sk, self_pay_req).await; - assert!(resp.is_ok()); - - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn allow_consecutive_full_transfer_bidirectional() -> anyhow::Result<()> { - // 1. Initial Setup - let another_wallet = "bcrt1pps5lhtvuf0la6zq2dgu5h2q2tjdvc7sndkqj5x45qx303p2ln8yslk92wv"; - let wallet_a = new_mnemonic(&SecretString(String::new())).await?; - let wallet_b = new_mnemonic(&SecretString(String::new())).await?; - - let wallet_a_desc = &wallet_a.public.btc_descriptor_xpub; - let wallet_b_desc = &wallet_b.public.btc_descriptor_xpub; - - let wallet_a_change_desc = &wallet_a.public.btc_change_descriptor_xpub; - let wallet_b_change_desc = &wallet_b.public.btc_change_descriptor_xpub; - - let wallet_a_vault = get_wallet(&SecretString(wallet_a_desc.to_string()), None).await?; - let wallet_b_vault = get_wallet(&SecretString(wallet_b_desc.to_string()), None).await?; - - let wallet_a_address = &wallet_a_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - let wallet_b_address = &wallet_b_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - send_some_coins(wallet_a_address, "1").await; - send_some_coins(wallet_b_address, "1").await; - - let wallet_a_sk = &wallet_a.clone().private.nostr_prv; - let wallet_b_sk = &wallet_b.clone().private.nostr_prv; - - let wallet_a_watcher = &wallet_a.clone().public.watcher_xpub; - let create_watcher_a = WatcherRequest { - name: "default".to_string(), - xpub: wallet_a_watcher.to_string(), - force: false, - }; - let wallet_b_watcher = &wallet_b.clone().public.watcher_xpub; - let create_watcher_b = WatcherRequest { - name: "default".to_string(), - xpub: wallet_b_watcher.to_string(), - force: false, - }; - - let _ = create_watcher(wallet_a_sk, create_watcher_a).await; - let _ = create_watcher(wallet_b_sk, create_watcher_b).await; - - // 2. Generate Funded Vault - let wallet_a_desc = &wallet_a.public.rgb_assets_descriptor_xpub; - let wallet_b_desc = &wallet_b.public.rgb_assets_descriptor_xpub; - - let wallet_a_vault = get_wallet( - &SecretString(wallet_a_desc.to_string()), - Some(&SecretString(wallet_a_change_desc.to_string())), - ) - .await?; - let wallet_b_vault = get_wallet( - &SecretString(wallet_b_desc.to_string()), - Some(&SecretString(wallet_b_change_desc.to_string())), - ) - .await?; - - let wallet_a_address = &wallet_a_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - let wallet_b_address = &wallet_b_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - send_some_coins(wallet_a_address, "0.00000546").await; - send_some_coins(wallet_a_address, "0.00000546").await; - send_some_coins(wallet_b_address, "0.00000546").await; - send_some_coins(wallet_b_address, "0.00000546").await; - sync_wallet(&wallet_a_vault).await?; - sync_wallet(&wallet_b_vault).await?; - - // 3. - let contract_issue = issuer_issue_contract_v2( - 1, - "RGB20", - 15_000, - false, - false, - None, - None, - Some(UtxoFilter::with_amount_equal_than(546)), - Some(wallet_a.clone()), - ) - .await?; - let contract_issue = contract_issue[0].clone(); - - let IssueResponse { - contract_id, - iimpl_id: _, - iface, - issue_method: _, - issue_utxo: _, - ticker: _, - name: _, - created: _, - description: _, - supply: _, - precision: _, - contract, - genesis: _, - meta: _, - } = contract_issue; - - // 4. Make a Self Payment - for i in 1..10 { - // Wallet B Invoice - sync_wallet(&wallet_b_vault).await?; - let utxo_unspent = wallet_b_vault.lock().await.list_unspent().expect(""); - let utxo_unspent = utxo_unspent.last().unwrap(); - let wallet_b_invoice = create_new_invoice_v2( - &contract_id.to_string(), - &iface.to_string(), - 1_000, - &utxo_unspent.outpoint.to_string(), - wallet_b.clone(), - None, - Some(contract.armored.clone()), - ) - .await?; - - let self_pay_req = FullRgbTransferRequest { - contract_id: contract_id.clone(), - iface: iface.clone(), - rgb_invoice: wallet_b_invoice.invoice, - descriptor: SecretString(wallet_a_desc.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(546), - bitcoin_changes: vec![], - }; - - let full_transfer_resp = full_transfer_asset(wallet_a_sk, self_pay_req).await; - println!("Payment A #{i}:1 ({})", full_transfer_resp.is_ok()); - - let full_transfer_resp = full_transfer_resp?; - let RgbTransferResponse { - consig_id: _, - consig, - psbt, - commit: _, - } = full_transfer_resp; - - let request = SignPsbtRequest { - psbt, - descriptors: vec![ - SecretString(wallet_a.private.rgb_assets_descriptor_xprv.clone()), - SecretString(wallet_a.private.btc_descriptor_xprv.clone()), - SecretString(wallet_a.private.btc_change_descriptor_xprv.clone()), - ], - }; - let resp = sign_psbt_file(request).await; - // println!("{:#?}", resp); - assert!(resp.is_ok()); - - let request = AcceptRequest { - consignment: consig.clone(), - force: false, - }; - let resp = accept_transfer(wallet_a_sk, request.clone()).await; - assert!(resp.is_ok()); - let request = AcceptRequest { - consignment: consig.clone(), - force: false, - }; - let resp = accept_transfer(wallet_b_sk, request.clone()).await; - assert!(resp.is_ok()); - - send_some_coins(another_wallet, "0.00000546").await; - - for j in 1..2 { - // Wallet A Invoice - sync_wallet(&wallet_a_vault).await?; - let utxo_unspent = wallet_a_vault.lock().await.list_unspent().expect(""); - let utxo_unspent = utxo_unspent.last().unwrap(); - let wallet_a_invoice = create_new_invoice_v2( - &contract_id.to_string(), - &iface.to_string(), - 50, - &utxo_unspent.outpoint.to_string(), - wallet_a.clone(), - None, - Some(contract.armored.clone()), - ) - .await?; - - let self_pay_req = FullRgbTransferRequest { - contract_id: contract_id.clone(), - iface: iface.clone(), - rgb_invoice: wallet_a_invoice.invoice, - descriptor: SecretString(wallet_b_desc.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(546), - bitcoin_changes: vec![], - }; - - let full_transfer_resp = full_transfer_asset(wallet_b_sk, self_pay_req).await; - println!("Payment B #{i}:{j} ({})", full_transfer_resp.is_ok()); - - let full_transfer_resp = full_transfer_resp?; - let RgbTransferResponse { - consig_id: _, - consig, - psbt, - commit: _, - } = full_transfer_resp; - - let request = SignPsbtRequest { - psbt, - descriptors: vec![ - SecretString(wallet_b.private.rgb_assets_descriptor_xprv.clone()), - SecretString(wallet_b.private.btc_descriptor_xprv.clone()), - SecretString(wallet_b.private.btc_change_descriptor_xprv.clone()), - ], - }; - let resp = sign_psbt_file(request).await; - // println!("{:#?}", resp); - assert!(resp.is_ok()); - - let request = AcceptRequest { - consignment: consig.clone(), - force: false, - }; - let resp = accept_transfer(wallet_a_sk, request.clone()).await; - assert!(resp.is_ok()); - let request = AcceptRequest { - consignment: consig.clone(), - force: false, - }; - let resp = accept_transfer(wallet_b_sk, request.clone()).await; - assert!(resp.is_ok()); - } - - send_some_coins(another_wallet, "0.00000546").await; - } - - let _contract_a = get_contract(wallet_a_sk, &contract_id).await?; - let _contract_b = get_contract(wallet_b_sk, &contract_id).await?; - - // println!( - // "Contract A: {}\nAllocations: {:#?}\n\n", - // contract_a.contract_id, contract_a.allocations - // ); - // println!( - // "Contract B: {}\nAllocations: {:#?}", - // contract_b.contract_id, contract_b.allocations - // ); - - Ok(()) -} - -#[tokio::test] -async fn allow_save_transfer_and_verify() -> anyhow::Result<()> { - // 1. Initial Setup - let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; - let issuer_keys = save_mnemonic( - &SecretString(ISSUER_MNEMONIC.to_string()), - &SecretString("".to_string()), - ) - .await?; - let owner_keys = save_mnemonic( - &SecretString(OWNER_MNEMONIC.to_string()), - &SecretString("".to_string()), - ) - .await?; - let issuer_resp = issuer_issue_contract_v2( - 1, - "RGB20", - 1, - false, - true, - None, - Some("0.00000546".to_string()), - Some(UtxoFilter::with_amount_less_than(546)), - None, - ) - .await?; - - let issuer_watcher_key = &issuer_keys.public.watcher_xpub; - let issuer_watcher = WatcherRequest { - name: "default".to_string(), - xpub: issuer_watcher_key.to_string(), - force: false, - }; - let issue_sk = issuer_keys.private.nostr_prv.to_string(); - create_watcher(&issue_sk, issuer_watcher).await?; - - let owner_watcher_key = &owner_keys.public.watcher_xpub; - let owner_watcher = WatcherRequest { - name: "default".to_string(), - xpub: owner_watcher_key.to_string(), - force: false, - }; - let owner_sk = owner_keys.private.nostr_prv.to_string(); - create_watcher(&owner_sk, owner_watcher).await?; - - // 2. Get Invoice - let issuer_resp = issuer_resp[0].clone(); - - let owner_resp = &create_new_invoice( - &issuer_resp.contract_id, - &issuer_resp.iface, - 1, - owner_keys.clone(), - None, - Some(issuer_resp.clone().contract.strict), - ) - .await?; - - // 3. Get Bitcoin UTXO - let issuer_btc_desc = &issuer_keys.public.btc_change_descriptor_xpub; - let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; - let issuer_address = &issuer_vault - .lock() - .await - .get_address(AddressIndex::LastUnused)? - .address - .to_string(); - - send_some_coins(issuer_address, "0.001").await; - sync_wallet(&issuer_vault).await?; - - // 4. Make a Self Payment - let self_pay_req = FullRgbTransferRequest { - contract_id: issuer_resp.contract_id.clone(), - iface: issuer_resp.iface.clone(), - rgb_invoice: owner_resp.invoice.to_string(), - descriptor: SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(1000), - bitcoin_changes: vec![], - }; - - let resp = full_transfer_asset(&issue_sk, self_pay_req).await?; - - let RgbTransferResponse { - consig_id: _, - consig, - psbt, - commit: _, - } = resp; - - let request = SignPsbtRequest { - psbt, - descriptors: vec![ - SecretString(issuer_keys.private.rgb_assets_descriptor_xprv.clone()), - SecretString(issuer_keys.private.btc_descriptor_xprv.clone()), - SecretString(issuer_keys.private.btc_change_descriptor_xprv.clone()), - ], - }; - let resp = sign_psbt_file(request).await; - assert!(resp.is_ok()); - send_some_coins(whatever_address, "0.001").await; - - let owner_sk = owner_keys.private.nostr_prv.clone(); - let request = RgbSaveTransferRequest { - iface: issuer_resp.iface.clone(), - contract_id: issuer_resp.contract_id.clone(), - consignment: consig, - }; - let resp = save_transfer(&owner_sk, request).await; - assert!(resp.is_ok()); - - let resp = verify_transfers(&owner_sk).await; - assert!(resp.is_ok()); - - let contract = get_contract(&owner_sk, &issuer_resp.contract_id).await?; - assert_eq!(contract.balance, 1); - - Ok(()) -} diff --git a/tests/rgb/integration/crdt.rs b/tests/rgb/integration/crdt.rs new file mode 100644 index 00000000..e4fcfcbd --- /dev/null +++ b/tests/rgb/integration/crdt.rs @@ -0,0 +1,77 @@ +#![cfg(not(target_arch = "wasm32"))] +use crate::rgb::integration::utils::get_raw_wallet; +use amplify::confinement::Collection; +use automerge::AutoCommit; +use autosurgeon::{hydrate, reconcile}; +use bitmask_core::rgb::crdt::{RawRgbWallet, RawUtxo}; +use rgb::{RgbWallet, Utxo}; + +#[tokio::test] +async fn allow_fork_with_previous_version() -> anyhow::Result<()> { + let current_raw_wallet = get_raw_wallet(); + let new_raw_utxo = RawUtxo { + outpoint: "9a5d21d4cc15ffa14c6f416396235c082cddb5e227abd863974445709f8e9af0:0".to_string(), + block: 0, + amount: 10000000, + terminal: "20:1".to_string(), + tweak: Some("tapret02p3PfJiAs6WexAGDDKLyfRqfFaoPG5VFoc6gnqw3q9zQt1shWmF".to_string()), + }; + + let new_utxo = Utxo::from(new_raw_utxo); + let mut rgb_wallet = RgbWallet::from(current_raw_wallet.clone()); + + let mut original = AutoCommit::new(); + reconcile(&mut original, current_raw_wallet)?; + + let mut fork = original.fork(); + + rgb_wallet.utxos.push(new_utxo.clone()); + let latest = RawRgbWallet::from(rgb_wallet); + reconcile(&mut fork, latest)?; + + original.merge(&mut fork)?; + + let merged: RawRgbWallet = hydrate(&original).unwrap(); + let merged = RgbWallet::from(merged); + + assert!(merged.utxo(new_utxo.outpoint).is_some()); + + Ok(()) +} + +#[tokio::test] +async fn allow_fork_without_previous_version() -> anyhow::Result<()> { + let current_raw_wallet = get_raw_wallet(); + let new_raw_utxo = RawUtxo { + outpoint: "9a5d21d4cc15ffa14c6f416396235c082cddb5e227abd863974445709f8e9af0:0".to_string(), + block: 0, + amount: 10000000, + terminal: "20:1".to_string(), + tweak: Some("tapret02p3PfJiAs6WexAGDDKLyfRqfFaoPG5VFoc6gnqw3q9zQt1shWmF".to_string()), + }; + + let new_utxo = Utxo::from(new_raw_utxo); + let mut rgb_wallet = RgbWallet::from(current_raw_wallet.clone()); + + // Create Original + let mut original = AutoCommit::new(); + reconcile(&mut original, current_raw_wallet.clone())?; + + // Rebase Fork + let mut fork = original.fork(); + + rgb_wallet.utxos.push(new_utxo.clone()); + let latest = RawRgbWallet::from(rgb_wallet); + reconcile(&mut fork, latest)?; + + // Rebase Original + let mut original = AutoCommit::new(); + original.merge(&mut fork)?; + + let merged: RawRgbWallet = hydrate(&original).unwrap(); + let merged = RgbWallet::from(merged); + + assert!(merged.utxo(new_utxo.outpoint).is_some()); + + Ok(()) +} diff --git a/tests/rgb/integration/fungibles.rs b/tests/rgb/integration/fungibles.rs index 42b04480..22f7d62d 100644 --- a/tests/rgb/integration/fungibles.rs +++ b/tests/rgb/integration/fungibles.rs @@ -57,7 +57,7 @@ async fn allow_beneficiary_accept_transfer() -> anyhow::Result<()> { let sk = issuer_keys.private.nostr_prv.to_string(); let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), diff --git a/tests/rgb/integration/states.rs b/tests/rgb/integration/states.rs index c83a0203..654b7204 100644 --- a/tests/rgb/integration/states.rs +++ b/tests/rgb/integration/states.rs @@ -84,7 +84,7 @@ async fn check_fungible_state_after_accept_consig() -> anyhow::Result<()> { let owner_sk = owner_keys.clone().private.nostr_prv.to_string(); let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -187,7 +187,7 @@ async fn check_uda_state_after_accept_consig() -> anyhow::Result<()> { let owner_sk = owner_keys.clone().private.nostr_prv.to_string(); let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_udas_descriptor_xprv.clone(), )] .to_vec(), diff --git a/tests/rgb/integration/transfers.rs b/tests/rgb/integration/transfers.rs index bb2279b8..735c6abf 100644 --- a/tests/rgb/integration/transfers.rs +++ b/tests/rgb/integration/transfers.rs @@ -1,15 +1,18 @@ #![cfg(not(target_arch = "wasm32"))] use std::collections::{BTreeSet, HashMap}; +use bdk::wallet::AddressIndex; use bitmask_core::{ - bitcoin::{save_mnemonic, sign_psbt_file}, + bitcoin::{get_wallet, new_mnemonic, save_mnemonic, sign_psbt_file, sync_wallet}, rgb::{ - accept_transfer, create_invoice, create_watcher, get_contract, import, - watcher_next_address, watcher_next_utxo, watcher_unspent_utxos, + accept_transfer, create_invoice, create_watcher, full_transfer_asset, get_contract, import, + save_transfer, verify_transfers, watcher_next_address, watcher_next_utxo, + watcher_unspent_utxos, }, structs::{ - AcceptRequest, AllocationDetail, AssetType, DecryptedWalletData, ImportRequest, - InvoiceRequest, PsbtFeeRequest, SecretString, SignPsbtRequest, WatcherRequest, + AcceptRequest, AllocationDetail, AssetType, DecryptedWalletData, FullRgbTransferRequest, + ImportRequest, InvoiceRequest, IssueResponse, PsbtFeeRequest, RgbSaveTransferRequest, + RgbTransferResponse, SecretString, SignPsbtRequest, WatcherRequest, }, }; @@ -95,7 +98,7 @@ async fn allow_issuer_make_conseq_transfers() -> anyhow::Result<()> { let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -165,7 +168,7 @@ async fn allow_issuer_make_conseq_transfers() -> anyhow::Result<()> { &create_new_transfer(issuer_keys.clone(), invoice_2.clone(), psbt_resp.clone()).await?; let request: SignPsbtRequest = SignPsbtRequest { psbt: issuer_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -250,7 +253,7 @@ async fn allow_owner_make_conseq_transfers() -> anyhow::Result<()> { // 2. Sign and Publish TX (Issuer side) let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -328,7 +331,7 @@ async fn allow_owner_make_conseq_transfers() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( owner_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -393,7 +396,7 @@ async fn allow_owner_make_conseq_transfers() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( owner_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -509,7 +512,7 @@ async fn allow_conseq_transfers_between_tree_owners() -> anyhow::Result<()> { let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_assets_descriptor_xprv.clone(), )] .to_vec(), @@ -588,7 +591,7 @@ async fn allow_conseq_transfers_between_tree_owners() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: issuer_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString(issuer_xpriv)].to_vec(), + descriptors: [SecretString(issuer_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -617,7 +620,7 @@ async fn allow_conseq_transfers_between_tree_owners() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: owner_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString(owner_xpriv)].to_vec(), + descriptors: [SecretString(owner_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -759,7 +762,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { let issuer_xprv = issuer_keys.private.rgb_assets_descriptor_xprv.clone(); let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString(issuer_xprv)].to_vec(), + descriptors: [SecretString(issuer_xprv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -842,7 +845,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: issuer_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString(issuer_xpriv)].to_vec(), + descriptors: [SecretString(issuer_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -871,7 +874,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: owner_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString(owner_xpriv)].to_vec(), + descriptors: [SecretString(owner_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -951,7 +954,7 @@ async fn allows_spend_amount_from_two_different_owners() -> anyhow::Result<()> { .await?; let request = SignPsbtRequest { psbt: another_transfer_to_issuer.psbt.clone(), - descriptors: vec![SecretString(another_owner_xpriv)].to_vec(), + descriptors: [SecretString(another_owner_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -1066,7 +1069,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< let issuer_xprv = issuer_keys.private.rgb_assets_descriptor_xprv.clone(); let request = SignPsbtRequest { psbt: transfer_resp.psbt.clone(), - descriptors: vec![SecretString(issuer_xprv)].to_vec(), + descriptors: [SecretString(issuer_xprv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -1149,7 +1152,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< .await?; let request = SignPsbtRequest { psbt: issuer_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString(issuer_xpriv)].to_vec(), + descriptors: [SecretString(issuer_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -1177,7 +1180,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< .await?; let request = SignPsbtRequest { psbt: owner_transfer_to_another_resp.psbt.clone(), - descriptors: vec![SecretString(owner_xpriv)].to_vec(), + descriptors: [SecretString(owner_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -1268,7 +1271,7 @@ async fn allows_spend_amount_from_two_different_transitions() -> anyhow::Result< .await?; let request = SignPsbtRequest { psbt: another_transfer_to_issuer.psbt.clone(), - descriptors: vec![SecretString(another_owner_xpriv)].to_vec(), + descriptors: [SecretString(another_owner_xpriv)].to_vec(), }; let resp = sign_psbt_file(request).await; assert!(resp.is_ok()); @@ -1656,3 +1659,532 @@ async fn allow_issuer_make_transfer_of_two_contract_types_in_same_utxo() -> anyh Ok(()) } + +#[ignore] +#[tokio::test] +async fn allow_fungible_full_transfer_op() -> anyhow::Result<()> { + // 1. Initial Setup + let issuer_keys = save_mnemonic( + &SecretString(ISSUER_MNEMONIC.to_string()), + &SecretString("".to_string()), + ) + .await?; + let owner_keys = save_mnemonic( + &SecretString(OWNER_MNEMONIC.to_string()), + &SecretString("".to_string()), + ) + .await?; + let issuer_resp = issuer_issue_contract_v2( + 1, + "RGB20", + 1, + false, + true, + None, + Some("0.00000546".to_string()), + Some(UtxoFilter::with_amount_less_than(546)), + None, + ) + .await?; + + // 2. Get Invoice + let issuer_resp = issuer_resp[0].clone(); + let owner_resp = &create_new_invoice( + &issuer_resp.contract_id, + &issuer_resp.iface, + 1, + owner_keys.clone(), + None, + Some(issuer_resp.clone().contract.strict), + ) + .await?; + + // 3. Get Bitcoin UTXO + let issuer_btc_desc = &issuer_keys.public.btc_change_descriptor_xpub; + let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; + let issuer_address = &issuer_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + send_some_coins(issuer_address, "0.001").await; + sync_wallet(&issuer_vault).await?; + + // 4. Make a Self Payment + let self_pay_req = FullRgbTransferRequest { + contract_id: issuer_resp.contract_id, + iface: issuer_resp.iface, + rgb_invoice: owner_resp.invoice.to_string(), + descriptor: SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.to_string()), + change_terminal: "/20/1".to_string(), + fee: PsbtFeeRequest::Value(1000), + bitcoin_changes: vec![], + }; + + let issue_sk = issuer_keys.private.nostr_prv.to_string(); + let resp = full_transfer_asset(&issue_sk, self_pay_req).await; + + assert!(resp.is_ok()); + Ok(()) +} + +#[ignore] +#[tokio::test] +async fn allow_uda_full_transfer_op() -> anyhow::Result<()> { + // 1. Initial Setup + let issuer_keys = save_mnemonic( + &SecretString(ISSUER_MNEMONIC.to_string()), + &SecretString("".to_string()), + ) + .await?; + let owner_keys = save_mnemonic( + &SecretString(OWNER_MNEMONIC.to_string()), + &SecretString("".to_string()), + ) + .await?; + let meta = Some(get_uda_data()); + let issuer_resp = issuer_issue_contract_v2( + 1, + "RGB21", + 1, + false, + true, + meta, + Some("0.00000546".to_string()), + Some(UtxoFilter::with_amount_less_than(546)), + None, + ) + .await?; + + // 2. Get Invoice + let issuer_resp = issuer_resp[0].clone(); + let owner_resp = &create_new_invoice( + &issuer_resp.contract_id, + &issuer_resp.iface, + 1, + owner_keys.clone(), + None, + None, + ) + .await?; + + // 3. Get Bitcoin UTXO + let issuer_btc_desc = &issuer_keys.public.btc_change_descriptor_xpub; + let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; + let issuer_address = &issuer_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + send_some_coins(issuer_address, "0.001").await; + sync_wallet(&issuer_vault).await?; + + // 4. Make a Self Payment + let self_pay_req = FullRgbTransferRequest { + contract_id: issuer_resp.contract_id, + iface: issuer_resp.iface, + rgb_invoice: owner_resp.invoice.to_string(), + descriptor: SecretString(issuer_keys.public.rgb_udas_descriptor_xpub.to_string()), + change_terminal: "/21/1".to_string(), + fee: PsbtFeeRequest::Value(546), + bitcoin_changes: vec![], + }; + + let issue_sk = issuer_keys.private.nostr_prv.to_string(); + let resp = full_transfer_asset(&issue_sk, self_pay_req).await; + assert!(resp.is_ok()); + + Ok(()) +} + +#[ignore] +#[tokio::test] +async fn allow_consecutive_full_transfer_bidirectional() -> anyhow::Result<()> { + // 1. Initial Setup + let another_wallet = "bcrt1pps5lhtvuf0la6zq2dgu5h2q2tjdvc7sndkqj5x45qx303p2ln8yslk92wv"; + let wallet_a = new_mnemonic(&SecretString(String::new())).await?; + let wallet_b = new_mnemonic(&SecretString(String::new())).await?; + + let wallet_a_desc = &wallet_a.public.btc_descriptor_xpub; + let wallet_b_desc = &wallet_b.public.btc_descriptor_xpub; + + let wallet_a_change_desc = &wallet_a.public.btc_change_descriptor_xpub; + let wallet_b_change_desc = &wallet_b.public.btc_change_descriptor_xpub; + + let wallet_a_vault = get_wallet(&SecretString(wallet_a_desc.to_string()), None).await?; + let wallet_b_vault = get_wallet(&SecretString(wallet_b_desc.to_string()), None).await?; + + let wallet_a_address = &wallet_a_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + let wallet_b_address = &wallet_b_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + send_some_coins(wallet_a_address, "1").await; + send_some_coins(wallet_b_address, "1").await; + + let wallet_a_sk = &wallet_a.clone().private.nostr_prv; + let wallet_b_sk = &wallet_b.clone().private.nostr_prv; + + let wallet_a_watcher = &wallet_a.clone().public.watcher_xpub; + let create_watcher_a = WatcherRequest { + name: "default".to_string(), + xpub: wallet_a_watcher.to_string(), + force: false, + }; + let wallet_b_watcher = &wallet_b.clone().public.watcher_xpub; + let create_watcher_b = WatcherRequest { + name: "default".to_string(), + xpub: wallet_b_watcher.to_string(), + force: false, + }; + + let _ = create_watcher(wallet_a_sk, create_watcher_a).await; + let _ = create_watcher(wallet_b_sk, create_watcher_b).await; + + // 2. Generate Funded Vault + let wallet_a_desc = &wallet_a.public.rgb_assets_descriptor_xpub; + let wallet_b_desc = &wallet_b.public.rgb_assets_descriptor_xpub; + + let wallet_a_vault = get_wallet( + &SecretString(wallet_a_desc.to_string()), + Some(&SecretString(wallet_a_change_desc.to_string())), + ) + .await?; + let wallet_b_vault = get_wallet( + &SecretString(wallet_b_desc.to_string()), + Some(&SecretString(wallet_b_change_desc.to_string())), + ) + .await?; + + let wallet_a_address = &wallet_a_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + let wallet_b_address = &wallet_b_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + send_some_coins(wallet_a_address, "0.00000546").await; + send_some_coins(wallet_a_address, "0.00000546").await; + send_some_coins(wallet_b_address, "0.00000546").await; + send_some_coins(wallet_b_address, "0.00000546").await; + sync_wallet(&wallet_a_vault).await?; + sync_wallet(&wallet_b_vault).await?; + + // 3. + let contract_issue = issuer_issue_contract_v2( + 1, + "RGB20", + 15_000, + false, + false, + None, + None, + Some(UtxoFilter::with_amount_equal_than(546)), + Some(wallet_a.clone()), + ) + .await?; + let contract_issue = contract_issue[0].clone(); + + let IssueResponse { + contract_id, + iimpl_id: _, + iface, + issue_method: _, + issue_utxo: _, + ticker: _, + name: _, + created: _, + description: _, + supply: _, + precision: _, + contract, + genesis: _, + meta: _, + } = contract_issue; + + // 4. Make a Self Payment + for _ in 1..10 { + // Wallet B Invoice + sync_wallet(&wallet_b_vault).await?; + let utxo_unspent = wallet_b_vault.lock().await.list_unspent().expect(""); + let utxo_unspent = utxo_unspent.last().unwrap(); + let wallet_b_invoice = create_new_invoice_v2( + &contract_id.to_string(), + &iface.to_string(), + 1_000, + &utxo_unspent.outpoint.to_string(), + wallet_b.clone(), + None, + Some(contract.armored.clone()), + ) + .await?; + + let self_pay_req = FullRgbTransferRequest { + contract_id: contract_id.clone(), + iface: iface.clone(), + rgb_invoice: wallet_b_invoice.invoice, + descriptor: SecretString(wallet_a_desc.to_string()), + change_terminal: "/20/1".to_string(), + fee: PsbtFeeRequest::Value(546), + bitcoin_changes: vec![], + }; + + let full_transfer_resp = full_transfer_asset(wallet_a_sk, self_pay_req).await; + // println!("Payment A #{i}:1 ({})", full_transfer_resp.is_ok()); + + let full_transfer_resp = full_transfer_resp?; + let RgbTransferResponse { + consig_id: _, + consig, + psbt, + commit: _, + } = full_transfer_resp; + + let request = SignPsbtRequest { + psbt, + descriptors: vec![ + SecretString(wallet_a.private.rgb_assets_descriptor_xprv.clone()), + SecretString(wallet_a.private.btc_descriptor_xprv.clone()), + SecretString(wallet_a.private.btc_change_descriptor_xprv.clone()), + ], + }; + let resp = sign_psbt_file(request).await; + // println!("{:#?}", resp); + assert!(resp.is_ok()); + + let request = AcceptRequest { + consignment: consig.clone(), + force: false, + }; + let resp = accept_transfer(wallet_a_sk, request.clone()).await; + assert!(resp.is_ok()); + let request = AcceptRequest { + consignment: consig.clone(), + force: false, + }; + let resp = accept_transfer(wallet_b_sk, request.clone()).await; + assert!(resp.is_ok()); + + send_some_coins(another_wallet, "0.00000546").await; + + for _ in 1..2 { + // Wallet A Invoice + sync_wallet(&wallet_a_vault).await?; + let utxo_unspent = wallet_a_vault.lock().await.list_unspent().expect(""); + let utxo_unspent = utxo_unspent.last().unwrap(); + let wallet_a_invoice = create_new_invoice_v2( + &contract_id.to_string(), + &iface.to_string(), + 50, + &utxo_unspent.outpoint.to_string(), + wallet_a.clone(), + None, + Some(contract.armored.clone()), + ) + .await?; + + let self_pay_req = FullRgbTransferRequest { + contract_id: contract_id.clone(), + iface: iface.clone(), + rgb_invoice: wallet_a_invoice.invoice, + descriptor: SecretString(wallet_b_desc.to_string()), + change_terminal: "/20/1".to_string(), + fee: PsbtFeeRequest::Value(546), + bitcoin_changes: vec![], + }; + + let full_transfer_resp = full_transfer_asset(wallet_b_sk, self_pay_req).await; + // println!("Payment B #{i}:{j} ({})", full_transfer_resp.is_ok()); + + let full_transfer_resp = full_transfer_resp?; + let RgbTransferResponse { + consig_id: _, + consig, + psbt, + commit: _, + } = full_transfer_resp; + + let request = SignPsbtRequest { + psbt, + descriptors: vec![ + SecretString(wallet_b.private.rgb_assets_descriptor_xprv.clone()), + SecretString(wallet_b.private.btc_descriptor_xprv.clone()), + SecretString(wallet_b.private.btc_change_descriptor_xprv.clone()), + ], + }; + let resp = sign_psbt_file(request).await; + // println!("{:#?}", resp); + assert!(resp.is_ok()); + + let request = AcceptRequest { + consignment: consig.clone(), + force: false, + }; + let resp = accept_transfer(wallet_a_sk, request.clone()).await; + assert!(resp.is_ok()); + let request = AcceptRequest { + consignment: consig.clone(), + force: false, + }; + let resp = accept_transfer(wallet_b_sk, request.clone()).await; + assert!(resp.is_ok()); + } + + send_some_coins(another_wallet, "0.00000546").await; + } + + let _contract_a = get_contract(wallet_a_sk, &contract_id).await?; + let _contract_b = get_contract(wallet_b_sk, &contract_id).await?; + + // println!( + // "Contract A: {}\nAllocations: {:#?}\n\n", + // contract_a.contract_id, contract_a.allocations + // ); + // println!( + // "Contract B: {}\nAllocations: {:#?}", + // contract_b.contract_id, contract_b.allocations + // ); + + Ok(()) +} + +#[tokio::test] +async fn allow_save_transfer_and_verify() -> anyhow::Result<()> { + // 1. Initial Setup + let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; + let issuer_keys = save_mnemonic( + &SecretString(ISSUER_MNEMONIC.to_string()), + &SecretString("".to_string()), + ) + .await?; + let owner_keys = save_mnemonic( + &SecretString(OWNER_MNEMONIC.to_string()), + &SecretString("".to_string()), + ) + .await?; + let issuer_resp = issuer_issue_contract_v2( + 1, + "RGB20", + 1, + false, + true, + None, + Some("0.00000546".to_string()), + Some(UtxoFilter::with_amount_less_than(546)), + None, + ) + .await?; + + let issuer_watcher_key = &issuer_keys.public.watcher_xpub; + let issuer_watcher = WatcherRequest { + name: "default".to_string(), + xpub: issuer_watcher_key.to_string(), + force: false, + }; + let issue_sk = issuer_keys.private.nostr_prv.to_string(); + create_watcher(&issue_sk, issuer_watcher).await?; + + let owner_watcher_key = &owner_keys.public.watcher_xpub; + let owner_watcher = WatcherRequest { + name: "default".to_string(), + xpub: owner_watcher_key.to_string(), + force: false, + }; + let owner_sk = owner_keys.private.nostr_prv.to_string(); + create_watcher(&owner_sk, owner_watcher).await?; + + // 2. Get Invoice + let issuer_resp = issuer_resp[0].clone(); + + let owner_resp = &create_new_invoice( + &issuer_resp.contract_id, + &issuer_resp.iface, + 1, + owner_keys.clone(), + None, + Some(issuer_resp.clone().contract.strict), + ) + .await?; + + // 3. Get Bitcoin UTXO + let issuer_btc_desc = &issuer_keys.public.btc_change_descriptor_xpub; + let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; + let issuer_address = &issuer_vault + .lock() + .await + .get_address(AddressIndex::LastUnused)? + .address + .to_string(); + + send_some_coins(issuer_address, "0.001").await; + sync_wallet(&issuer_vault).await?; + + // 4. Make a Self Payment + let self_pay_req = FullRgbTransferRequest { + contract_id: issuer_resp.contract_id.clone(), + iface: issuer_resp.iface.clone(), + rgb_invoice: owner_resp.invoice.to_string(), + descriptor: SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.to_string()), + change_terminal: "/20/1".to_string(), + fee: PsbtFeeRequest::Value(1000), + bitcoin_changes: vec![], + }; + + let resp = full_transfer_asset(&issue_sk, self_pay_req).await?; + + let RgbTransferResponse { + consig_id: _, + consig, + psbt, + commit: _, + } = resp; + + let request = SignPsbtRequest { + psbt, + descriptors: vec![ + SecretString(issuer_keys.private.rgb_assets_descriptor_xprv.clone()), + SecretString(issuer_keys.private.btc_descriptor_xprv.clone()), + SecretString(issuer_keys.private.btc_change_descriptor_xprv.clone()), + ], + }; + let resp = sign_psbt_file(request).await; + assert!(resp.is_ok()); + send_some_coins(whatever_address, "0.001").await; + + let owner_sk = owner_keys.private.nostr_prv.clone(); + let request = RgbSaveTransferRequest { + iface: issuer_resp.iface.clone(), + contract_id: issuer_resp.contract_id.clone(), + consignment: consig, + }; + let resp = save_transfer(&owner_sk, request).await; + assert!(resp.is_ok()); + + let resp = verify_transfers(&owner_sk).await; + assert!(resp.is_ok()); + + let contract = get_contract(&owner_sk, &issuer_resp.contract_id).await?; + assert_eq!(contract.balance, 1); + + Ok(()) +} diff --git a/tests/rgb/integration/udas.rs b/tests/rgb/integration/udas.rs index ed7d0a9e..db58881f 100644 --- a/tests/rgb/integration/udas.rs +++ b/tests/rgb/integration/udas.rs @@ -56,7 +56,7 @@ async fn allow_beneficiary_accept_transfer() -> anyhow::Result<()> { let sk = issuer_keys.private.nostr_prv.to_string(); let request = SignPsbtRequest { psbt: transfer_resp.psbt, - descriptors: vec![SecretString( + descriptors: [SecretString( issuer_keys.private.rgb_udas_descriptor_xprv.clone(), )] .to_vec(), diff --git a/tests/rgb/integration/utils.rs b/tests/rgb/integration/utils.rs index 4fc86d45..2c0231a4 100644 --- a/tests/rgb/integration/utils.rs +++ b/tests/rgb/integration/utils.rs @@ -2,10 +2,12 @@ #![cfg(not(target_arch = "wasm32"))] use std::{collections::HashMap, env, process::Stdio}; +use amplify::bmap; use bdk::wallet::AddressIndex; use bitmask_core::{ bitcoin::{get_wallet, get_wallet_data, save_mnemonic, sync_wallet}, rgb::{ + crdt::{RawRgbWallet, RawUtxo}, create_invoice, create_psbt, create_watcher, import, issue_contract, transfer_asset, watcher_details, watcher_next_address, watcher_next_utxo, watcher_unspent_utxos, }, @@ -614,3 +616,49 @@ pub fn _get_collectible_data() -> IssueMetaRequest { }, ])) } + +pub fn get_raw_wallet() -> RawRgbWallet { + RawRgbWallet { + xpub: "tpubDCBwP45jcvCdTBZSxn8TcCyQGx5YgietksRRptV9YJ1xnom6edMwb2JcBnNU15t6TmotHETmgnvHQ2Nki7N7CsgFhka6D91UgMaEYpTRuSh".to_string(), + taprets: bmap!{ + "20:1".to_string() => vec![ + "tapret054mNJMWTUsdX429C87Zi2AFr25cNGo6pxWeoEEsPExx8YNdxnCk".to_string(), + "tapret02Mq47rPbJsankXcDd3PDQaBhL1iDnELdqV6xz5eUiZVJYhLeVwu".to_string(), + ], + }, + utxos: vec![ + RawUtxo { + outpoint: "38de2eb14e917a066cda3283a9409ca4742427b00b070bda055cb82334d2d309:1".to_string(), + block: 904, + amount: 1000, + terminal: "20:0".to_string(), + tweak: None, + }, + RawUtxo { + outpoint: "866c629038b8cca56908f5989a4a2dde7942b41f3093212980c1ce388207ddf0:0".to_string(), + block: 901, + amount: 1000, + terminal: "20:0".to_string(), + tweak: None, + }, + RawUtxo { + outpoint: "1ded068d67c5c16d8ed0c551502c855a5242f22eda06e1e129b396afb3ace9f3:0".to_string(), + block: 0, + amount: 10000000, + terminal: "20:1".to_string(), + tweak: Some( + "tapret054mNJMWTUsdX429C87Zi2AFr25cNGo6pxWeoEEsPExx8YNdxnCk".to_string(), + ), + }, + RawUtxo { + outpoint: "adddea20b187cce1f03c45f7f8023f88541cfdd24725bcf9bbfd9472ea512548:0".to_string(), + block: 0, + amount: 10000000, + terminal: "20:1".to_string(), + tweak: Some( + "tapret02Mq47rPbJsankXcDd3PDQaBhL1iDnELdqV6xz5eUiZVJYhLeVwu".to_string(), + ), + } + ], + } +} diff --git a/tests/rgb/web/transfers.rs b/tests/rgb/web/transfers.rs index 627b38cb..0fc7e7c1 100644 --- a/tests/rgb/web/transfers.rs +++ b/tests/rgb/web/transfers.rs @@ -8,6 +8,7 @@ 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}, @@ -22,6 +23,7 @@ use bitmask_core::{ web::{ bitcoin::{ decrypt_wallet, encrypt_wallet, get_assets_vault, get_wallet_data, hash_password, + new_mnemonic, }, json_parse, resolve, rgb::{ @@ -43,39 +45,59 @@ wasm_bindgen_test_configure!(run_in_browser); const ENCRYPTION_PASSWORD: &str = "hunter2"; const SEED_PASSWORD: &str = ""; +pub struct TransferRounds { + pub send_amount: u64, + pub is_issuer_sender: bool, +} + +impl TransferRounds { + pub fn with(send_amount: u64, is_issuer_sender: bool) -> Self { + TransferRounds { + send_amount, + is_issuer_sender, + } + } +} + #[wasm_bindgen_test] +#[allow(unused_assignments)] async fn create_contract_and_transfer() { set_panic_hook(); - let issuer_mnemonic = - "try engine hurt mushroom adapt club boring diagram barely rail cable vicious tower boss hurt"; - let owner_mnemonic = - "rally ready surround evil grace autumn merry lunch husband infant forum wet possible thought drink"; - let hash = hash_password(ENCRYPTION_PASSWORD.to_owned()); - - info!("Import wallet"); - let issuer_mnemonic = resolve(encrypt_wallet( - issuer_mnemonic.to_owned(), - hash.clone(), - SEED_PASSWORD.to_owned(), - )) - .await; - let issuer_mnemonic: SecretString = json_parse(&issuer_mnemonic); - - let owner_mnemonic = resolve(encrypt_wallet( - owner_mnemonic.to_owned(), - hash.clone(), - SEED_PASSWORD.to_owned(), - )) - .await; - let owner_mnemonic: SecretString = json_parse(&owner_mnemonic); - - info!("Get Issuer Vault"); - let issuer_vault: JsValue = - resolve(decrypt_wallet(hash.clone(), issuer_mnemonic.to_string())).await; + // let issuer_mnemonic = + // "try engine hurt mushroom adapt club boring diagram barely rail cable vicious tower boss hurt"; + // let owner_mnemonic = + // "rally ready surround evil grace autumn merry lunch husband infant forum wet possible thought drink"; + // let hash = hash_password(ENCRYPTION_PASSWORD.to_owned()); + + // info!("Import wallet"); + // let issuer_mnemonic = resolve(encrypt_wallet( + // issuer_mnemonic.to_owned(), + // hash.clone(), + // SEED_PASSWORD.to_owned(), + // )) + // .await; + // let issuer_mnemonic: SecretString = json_parse(&issuer_mnemonic); + + // let owner_mnemonic = resolve(encrypt_wallet( + // owner_mnemonic.to_owned(), + // hash.clone(), + // SEED_PASSWORD.to_owned(), + // )) + // .await; + // let owner_mnemonic: SecretString = json_parse(&owner_mnemonic); + + // info!("Get Issuer Vault"); + // let issuer_vault: JsValue = + // resolve(decrypt_wallet(hash.clone(), issuer_mnemonic.to_string())).await; + // let issuer_vault: DecryptedWalletData = json_parse(&issuer_vault); + + // info!("Get Owner Vault"); + // let owner_vault: JsValue = resolve(decrypt_wallet(hash, owner_mnemonic.to_string())).await; + // let owner_vault: DecryptedWalletData = json_parse(&owner_vault); + + let issuer_vault = resolve(new_mnemonic("".to_string())).await; let issuer_vault: DecryptedWalletData = json_parse(&issuer_vault); - - info!("Get Owner Vault"); - let owner_vault: JsValue = resolve(decrypt_wallet(hash, owner_mnemonic.to_string())).await; + let owner_vault = resolve(new_mnemonic("".to_string())).await; let owner_vault: DecryptedWalletData = json_parse(&owner_vault); info!("Create Issuer Watcher"); @@ -138,25 +160,23 @@ async fn create_contract_and_transfer() { let resp = send_coins(&owner_next_address.address, "1").await; debug!(format!("Owner Receive Bitcoin {:?}", resp)); - info!("Get UTXO (Issuer)"); + info!("Get UTXO (Owner)"); let next_utxo: JsValue = resolve(watcher_next_utxo( - issuer_sk.clone(), + owner_sk.clone(), watcher_name.to_string(), iface.to_string(), )) .await; - let issuer_next_utxo: NextUtxoResponse = json_parse(&next_utxo); - debug!(format!("UTXO (Issuer): {:?}", issuer_next_utxo.utxo)); + let owner_next_utxo: NextUtxoResponse = json_parse(&next_utxo); - info!("Get UTXO (Owner)"); + info!("Get UTXO (Issuer)"); let next_utxo: JsValue = resolve(watcher_next_utxo( - owner_sk.clone(), + issuer_sk.clone(), watcher_name.to_string(), iface.to_string(), )) .await; - let owner_next_utxo: NextUtxoResponse = json_parse(&next_utxo); - debug!(format!("UTXO (Owner): {:?}", owner_next_utxo.utxo)); + let issuer_next_utxo: NextUtxoResponse = json_parse(&next_utxo); assert!(issuer_next_utxo.utxo.is_some()); assert!(owner_next_utxo.utxo.is_some()); @@ -180,283 +200,194 @@ async fn create_contract_and_transfer() { let issue_resp: JsValue = resolve(issue_contract(issuer_sk.to_string(), issue_req)).await; let issuer_resp: IssueResponse = json_parse(&issue_resp); - info!("::: SEND INVOICE FIRST TIME :::"); - info!("Create Invoice (Owner)"); - let params = HashMap::new(); - let owner_utxo = owner_next_utxo.utxo.unwrap().outpoint.to_string(); - let owner_seal = format!("tapret1st:{owner_utxo}"); - let invoice_req = InvoiceRequest { - contract_id: issuer_resp.contract_id.to_string(), - iface: issuer_resp.iface.to_string(), - amount: 2000, - seal: owner_seal, - params, - }; - let invoice_req = serde_wasm_bindgen::to_value(&invoice_req).expect(""); - let invoice_resp: JsValue = - resolve(rgb_create_invoice(owner_sk.to_string(), invoice_req)).await; - let invoice_resp: InvoiceResponse = json_parse(&invoice_resp); - - info!("Create Payment (Issuer)"); - debug!(format!( - "Invoice (Issuer): {}", - invoice_resp.invoice.to_string() - )); - - let issuer_desc = issuer_vault.public.rgb_assets_descriptor_xpub.to_string(); - let full_transfer_req = FullRgbTransferRequest { - contract_id: issuer_resp.contract_id.to_string(), - iface: issuer_resp.iface.to_string(), - rgb_invoice: invoice_resp.invoice.to_string(), - descriptor: SecretString(issuer_desc.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(1000), - bitcoin_changes: vec![], - }; - - let full_transfer_req = serde_wasm_bindgen::to_value(&full_transfer_req).expect(""); - let full_transfer_resp: JsValue = resolve(full_transfer_asset( - issuer_sk.to_string(), - full_transfer_req, - )) - .await; - let full_transfer_resp: RgbTransferResponse = json_parse(&full_transfer_resp); - debug!(format!( - "Payment (Issuer): {:?}", - full_transfer_resp.consig_id - )); - - info!("Sign PSBT (Issuer)"); - let psbt_req = SignPsbtRequest { - psbt: full_transfer_resp.psbt, - descriptors: 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()), - ], - }; - - let psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); - let psbt_resp: JsValue = resolve(psbt_sign_file(issuer_sk.to_string(), psbt_req)).await; - let psbt_resp: SignPsbtResponse = json_parse(&psbt_resp); - debug!(format!("Sign Psbt: {:?}", psbt_resp)); - - info!("Create new Block"); - let resp = new_block().await; - debug!(format!("Block Created: {:?}", resp)); - - info!("Save Consig (Owner)"); - let all_sks = [owner_sk.clone()]; - for sk in all_sks { - let save_transfer_req = RgbSaveTransferRequest { - iface: issuer_resp.iface.clone(), - contract_id: issuer_resp.contract_id.clone(), - consignment: full_transfer_resp.consig.clone(), + let mut total_issuer = supply; + let mut total_owner = 0; + let rounds = vec![ + TransferRounds::with(20, true), + TransferRounds::with(20, false), + // TransferRounds::with(3_000, true), + // TransferRounds::with(5_000, true), + // TransferRounds::with(20, true), + // TransferRounds::with(8_000, false), + // TransferRounds::with(9_000, true), + // TransferRounds::with(9_000, false), + // TransferRounds::with(9_000, true), + // TransferRounds::with(20, false), + // TransferRounds::with(50_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(); + 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(); + } 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(); + } + + info!(format!( + ">>>> ROUND #{index} {sender} SEND {} units to {receiver} <<<<", + round.send_amount + )); + info!(format!("Get Receiver Next UTXO ({receiver})")); + let next_utxo: JsValue = resolve(watcher_next_utxo( + receiver_sk.clone(), + watcher_name.to_string(), + iface.to_string(), + )) + .await; + let receiver_next_utxo: NextUtxoResponse = json_parse(&next_utxo); + debug!(format!("UTXO ({receiver}): {:?}", receiver_next_utxo.utxo)); + + info!(format!("Create Invoice ({receiver})")); + let params = HashMap::new(); + let receiver_utxo = receiver_next_utxo.utxo.unwrap().outpoint.to_string(); + let receiver_seal = format!("tapret1st:{receiver_utxo}"); + let invoice_req = InvoiceRequest { + contract_id: issuer_resp.contract_id.to_string(), + iface: issuer_resp.iface.to_string(), + amount: round.send_amount, + seal: receiver_seal, + params, }; - let save_transfer_req = serde_wasm_bindgen::to_value(&save_transfer_req).expect(""); - let save_transfer_resp = resolve(save_transfer(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 all_sks = [owner_sk.clone(), issuer_sk.clone()]; - for sk in all_sks { - let verify_transfer_resp = resolve(verify_transfers(sk.to_string())).await; - let verify_transfer_resp: BatchRgbTransferResponse = json_parse(&verify_transfer_resp); - debug!(format!("Verify Consig: {:?}", verify_transfer_resp)); - } - info!("::: SEND INVOICE SECOND TIME :::"); - info!("Create Invoice (Owner)"); - let params = HashMap::new(); - let owner_seal = format!("tapret1st:{owner_utxo}"); - let invoice_req = InvoiceRequest { - contract_id: issuer_resp.contract_id.to_string(), - iface: issuer_resp.iface.to_string(), - amount: 3000, - seal: owner_seal, - params, - }; - let invoice_req = serde_wasm_bindgen::to_value(&invoice_req).expect(""); - let invoice_resp: JsValue = - resolve(rgb_create_invoice(owner_sk.to_string(), invoice_req)).await; - let invoice_resp: InvoiceResponse = json_parse(&invoice_resp); - - info!("Create Payment (Issuer)"); - let issuer_desc = issuer_vault.public.rgb_assets_descriptor_xpub.to_string(); - let full_transfer_req = FullRgbTransferRequest { - contract_id: issuer_resp.contract_id.to_string(), - iface: issuer_resp.iface.to_string(), - rgb_invoice: invoice_resp.invoice.to_string(), - descriptor: SecretString(issuer_desc.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(1000), - bitcoin_changes: vec![], - }; - - let full_transfer_req = serde_wasm_bindgen::to_value(&full_transfer_req).expect(""); - let full_transfer_resp: JsValue = resolve(full_transfer_asset( - issuer_sk.to_string(), - full_transfer_req, - )) - .await; - let full_transfer_resp: RgbTransferResponse = json_parse(&full_transfer_resp); - debug!(format!( - "Payment (Issuer): {:?}", - full_transfer_resp.consig_id - )); + let invoice_req = serde_wasm_bindgen::to_value(&invoice_req).expect(""); + let invoice_resp: JsValue = + resolve(rgb_create_invoice(receiver_sk.to_string(), invoice_req)).await; + let invoice_resp: InvoiceResponse = json_parse(&invoice_resp); + debug!(format!( + "Invoice ({receiver}): {}", + invoice_resp.invoice.to_string() + )); + + info!(format!("Create Payment ({sender})")); + let full_transfer_req = FullRgbTransferRequest { + contract_id: issuer_resp.contract_id.to_string(), + iface: issuer_resp.iface.to_string(), + rgb_invoice: invoice_resp.invoice.to_string(), + descriptor: SecretString(sender_desc.to_string()), + change_terminal: "/20/1".to_string(), + fee: PsbtFeeRequest::Value(1000), + bitcoin_changes: vec![], + }; - info!("Sign PSBT (Issuer)"); - let psbt_req = SignPsbtRequest { - psbt: full_transfer_resp.psbt, - descriptors: 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()), - ], - }; + let full_transfer_req = serde_wasm_bindgen::to_value(&full_transfer_req).expect(""); + let full_transfer_resp: JsValue = resolve(full_transfer_asset( + sender_sk.to_string(), + full_transfer_req, + )) + .await; + let full_transfer_resp: RgbTransferResponse = json_parse(&full_transfer_resp); + debug!(format!( + "Payment ({sender}): {:?}", + full_transfer_resp.consig_id + )); + + info!(format!("Sign PSBT ({sender})")); + let psbt_req = SignPsbtRequest { + psbt: full_transfer_resp.psbt, + descriptors: sender_keys, + }; - let psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); - let psbt_resp: JsValue = resolve(psbt_sign_file(issuer_sk.to_string(), psbt_req)).await; - let psbt_resp: SignPsbtResponse = json_parse(&psbt_resp); - debug!(format!("Sign Psbt: {:?}", psbt_resp)); + 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); + debug!(format!("Sign Psbt: {:?}", psbt_resp)); - info!("Create new Block"); - let resp = new_block().await; - debug!(format!("Block Created: {:?}", resp)); + info!("Create new Block"); + let resp = new_block().await; + debug!(format!("Block Created: {:?}", resp)); - info!("Save Consig (Owner)"); - let all_sks = [owner_sk.clone()]; - for sk in all_sks { + info!(format!("Save Consig ({receiver})")); let save_transfer_req = RgbSaveTransferRequest { iface: issuer_resp.iface.clone(), contract_id: issuer_resp.contract_id.clone(), consignment: full_transfer_resp.consig.clone(), }; let save_transfer_req = serde_wasm_bindgen::to_value(&save_transfer_req).expect(""); - let save_transfer_resp = resolve(save_transfer(sk.to_string(), save_transfer_req)).await; + 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 all_sks = [owner_sk.clone(), issuer_sk.clone()]; - for sk in all_sks { - let verify_transfer_resp = resolve(verify_transfers(sk.to_string())).await; + info!("Verify Consig (Both)"); + let verify_transfer_resp = resolve(verify_transfers(sender_sk.to_string())).await; let verify_transfer_resp: BatchRgbTransferResponse = json_parse(&verify_transfer_resp); - debug!(format!("Verify Consig: {:?}", verify_transfer_resp)); - } - - info!("::: SEND INVOICE TO ISSUER :::"); - 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!("UTXO (Issuer): {:?}", issuer_next_utxo.utxo)); - - info!("Create Invoice (Issuer)"); - let params = HashMap::new(); - let issuer_seal = issuer_next_utxo.utxo.unwrap().outpoint.to_string(); - let issuer_seal = format!("tapret1st:{issuer_seal}"); - let invoice_req = InvoiceRequest { - contract_id: issuer_resp.contract_id.to_string(), - iface: issuer_resp.iface.to_string(), - amount: 4000, - seal: issuer_seal, - params, - }; - let invoice_req = serde_wasm_bindgen::to_value(&invoice_req).expect(""); - let invoice_resp: JsValue = - resolve(rgb_create_invoice(issuer_sk.to_string(), invoice_req)).await; - let invoice_resp: InvoiceResponse = json_parse(&invoice_resp); - - info!("Create Payment (owner)"); - let owner_desc = owner_vault.public.rgb_assets_descriptor_xpub.to_string(); - let full_transfer_req = FullRgbTransferRequest { - contract_id: issuer_resp.contract_id.to_string(), - iface: issuer_resp.iface.to_string(), - rgb_invoice: invoice_resp.invoice.to_string(), - descriptor: SecretString(owner_desc.to_string()), - change_terminal: "/20/1".to_string(), - fee: PsbtFeeRequest::Value(1000), - bitcoin_changes: vec![], - }; - - let full_transfer_req = serde_wasm_bindgen::to_value(&full_transfer_req).expect(""); - let full_transfer_resp: JsValue = - resolve(full_transfer_asset(owner_sk.to_string(), full_transfer_req)).await; - let full_transfer_resp: RgbTransferResponse = json_parse(&full_transfer_resp); - debug!(format!( - "Payment (Issuer): {:?}", - full_transfer_resp.consig_id - )); - - info!("Sign PSBT (Issuer)"); - let psbt_req = SignPsbtRequest { - psbt: full_transfer_resp.psbt, - descriptors: 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()), - ], - }; - - let psbt_req = serde_wasm_bindgen::to_value(&psbt_req).expect(""); - let psbt_resp: JsValue = resolve(psbt_sign_file(owner_sk.to_string(), psbt_req)).await; - let psbt_resp: SignPsbtResponse = json_parse(&psbt_resp); - debug!(format!("Sign Psbt: {:?}", psbt_resp)); + debug!(format!( + "Verify Consig ({sender}): {:?}", + verify_transfer_resp + )); - info!("Create new Block"); - let resp = new_block().await; - debug!(format!("Block Created: {:?}", resp)); - - info!("Save Consig (Issuer)"); - let all_sks = [issuer_sk.clone()]; - for sk in all_sks { - let save_transfer_req = RgbSaveTransferRequest { - iface: issuer_resp.iface.clone(), - contract_id: issuer_resp.contract_id.clone(), - consignment: full_transfer_resp.consig.clone(), + let verify_transfer_resp = resolve(verify_transfers(receiver_sk.to_string())).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) }; - let save_transfer_req = serde_wasm_bindgen::to_value(&save_transfer_req).expect(""); - let save_transfer_resp = resolve(save_transfer(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 all_sks = [owner_sk.clone(), issuer_sk.clone()]; - for sk in all_sks { - let verify_transfer_resp = resolve(verify_transfers(sk.to_string())).await; - let verify_transfer_resp: BatchRgbTransferResponse = json_parse(&verify_transfer_resp); - debug!(format!("Verify Consig: {:?}", verify_transfer_resp)); + 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.to_string(), + 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); } - - info!("Get Contract (Issuer)"); - let contract_resp: JsValue = resolve(get_contract( - issuer_sk.to_string(), - issuer_resp.contract_id.clone(), - )) - .await; - let contract_resp: ContractResponse = json_parse(&contract_resp); - debug!(format!( - "Contract {}({})\n {:#?}", - contract_resp.contract_id, contract_resp.balance, contract_resp.allocations - )); - info!("Get Contract (Owner)"); - let contract_resp: JsValue = resolve(get_contract( - owner_sk.to_string(), - issuer_resp.contract_id.clone(), - )) - .await; - let contract_resp: ContractResponse = json_parse(&contract_resp); - debug!(format!( - "Contract {}({})\n {:#?}", - contract_resp.contract_id, contract_resp.balance, contract_resp.allocations - )); }