diff --git a/Cargo.lock b/Cargo.lock index 8a123bf0..e0e7394b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -624,6 +624,7 @@ dependencies = [ "postcard 1.0.4", "pretty_env_logger", "psbt", + "rand", "regex", "reqwest", "rgb-contracts", diff --git a/Cargo.toml b/Cargo.toml index 4fe74eb8..6eec0b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ futures = { version = "0.3.28", features = [ "executor", ], default-features = true } garde = { version = "0.11.2", features = ["derive"], default-features = false } +rand = "0.8.5" getrandom = { version = "0.2.10", features = ["js"] } hex = "0.4.3" indexmap = "1.9.3" diff --git a/src/bin/bitmaskd.rs b/src/bin/bitmaskd.rs index 4c5ac8ba..c28b5f0b 100644 --- a/src/bin/bitmaskd.rs +++ b/src/bin/bitmaskd.rs @@ -22,15 +22,16 @@ use bitmask_core::{ rgb::{ accept_transfer, clear_watcher as rgb_clear_watcher, create_invoice, create_psbt, create_watcher, full_transfer_asset, import as rgb_import, issue_contract, list_contracts, - list_interfaces, list_schemas, reissue_contract, transfer_asset, watcher_address, - watcher_details as rgb_watcher_details, watcher_next_address, watcher_next_utxo, - watcher_utxo, + list_interfaces, list_schemas, list_transfers as list_rgb_transfers, reissue_contract, + remove_transfer as remove_rgb_transfer, save_transfer as save_rgb_transfer, transfer_asset, + watcher_address, watcher_details as rgb_watcher_details, watcher_next_address, + watcher_next_utxo, watcher_utxo, }, structs::{ AcceptRequest, FileMetadata, FullRgbTransferRequest, ImportRequest, InvoiceRequest, IssueAssetRequest, IssueRequest, MediaInfo, PsbtFeeRequest, PsbtRequest, ReIssueRequest, - RgbTransferRequest, SecretString, SelfFullRgbTransferRequest, SelfIssueRequest, - SignPsbtRequest, WatcherRequest, + RgbRemoveTransferRequest, RgbSaveTransferRequest, RgbTransferRequest, SecretString, + SelfFullRgbTransferRequest, SelfIssueRequest, SignPsbtRequest, WatcherRequest, }, }; use carbonado::file; @@ -160,8 +161,9 @@ async fn self_pay( iface: self_pay_req.iface, rgb_invoice: self_pay_req.rgb_invoice, descriptor: SecretString(issuer_keys.public.rgb_udas_descriptor_xpub.clone()), - change_terminal: self_pay_req.terminal, fee, + change_terminal: self_pay_req.terminal, + bitcoin_changes: self_pay_req.bitcoin_changes, }; let transfer_res = full_transfer_asset(sk, request).await?; @@ -327,6 +329,42 @@ async fn register_utxo( Ok((StatusCode::OK, Json(resp))) } +async fn list_transfers( + TypedHeader(auth): TypedHeader>, + Path(contract_id): Path, +) -> Result { + info!("GET /transfers/{contract_id:?}"); + + let nostr_hex_sk = auth.token(); + let transfers_res = list_rgb_transfers(nostr_hex_sk, contract_id).await?; + + Ok((StatusCode::OK, Json(transfers_res))) +} + +async fn save_transfer( + TypedHeader(auth): TypedHeader>, + Json(request): Json, +) -> Result { + info!("POST /transfers {request:?}"); + + let nostr_hex_sk = auth.token(); + let import_res = save_rgb_transfer(nostr_hex_sk, request).await?; + + Ok((StatusCode::OK, Json(import_res))) +} + +async fn remove_transfer( + TypedHeader(auth): TypedHeader>, + Json(request): Json, +) -> Result { + info!("DELETE /transfers {request:?}"); + + let nostr_hex_sk = auth.token(); + let import_res = remove_rgb_transfer(nostr_hex_sk, request).await?; + + Ok((StatusCode::OK, Json(import_res))) +} + async fn co_store( Path((pk, name)): Path<(String, String)>, body: Bytes, @@ -533,6 +571,9 @@ async fn main() -> Result<()> { ) .route("/watcher/:name/:asset/utxo/:utxo", put(register_utxo)) .route("/watcher/:name", delete(clear_watcher)) + .route("/transfers/:id", get(list_transfers)) + .route("/transfers/", post(save_transfer)) + .route("/transfers/", delete(remove_transfer)) .route("/key/:pk", get(key)) .route("/carbonado/status", get(status)) .route("/carbonado/:pk/:name", post(co_store)) diff --git a/src/constants.rs b/src/constants.rs index 32e6454d..3bd3204c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -191,4 +191,5 @@ pub async fn set_env(key: &str, value: &str) { pub mod storage_keys { pub const ASSETS_STOCK: &str = "bitmask-fungible_assets_stock.c15"; pub const ASSETS_WALLETS: &str = "bitmask-fungible_assets_wallets.c15"; + pub const ASSETS_TRANSFERS: &str = "bitmask_assets_transfers.c15"; } diff --git a/src/rgb.rs b/src/rgb.rs index 0cf31bff..c2fb2d5f 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -1,16 +1,15 @@ -use std::{ - collections::{BTreeMap, HashSet}, - str::FromStr, -}; - use ::psbt::serialize::Serialize; -use amplify::hex::ToHex; +use amplify::{ + confinement::U32, + hex::{FromHex, ToHex}, +}; use anyhow::{anyhow, Result}; -use bitcoin::Network; +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::TerminalPath; use rgbstd::{ containers::BindleContent, @@ -19,6 +18,10 @@ use rgbstd::{ persistence::{Stash, Stock}, }; use rgbwallet::{psbt::DbcPsbtError, RgbInvoice}; +use std::{ + collections::{BTreeMap, HashSet}, + str::FromStr, +}; use strict_encoding::StrictSerialize; use thiserror::Error; @@ -38,7 +41,7 @@ pub mod wallet; use crate::{ constants::{ get_network, - storage_keys::{ASSETS_STOCK, ASSETS_WALLETS}, + storage_keys::{ASSETS_STOCK, ASSETS_TRANSFERS, ASSETS_WALLETS}, BITCOIN_EXPLORER_API, NETWORK, }, rgb::{ @@ -60,31 +63,33 @@ use crate::{ IssueMetaRequest, IssueMetadata, IssueRequest, IssueResponse, NewCollectible, NextAddressResponse, NextUtxoResponse, NextUtxosResponse, PsbtFeeRequest, PsbtInputRequest, PsbtRequest, PsbtResponse, ReIssueRequest, ReIssueResponse, RgbInvoiceResponse, - RgbTransferRequest, RgbTransferResponse, SchemaDetail, SchemasResponse, SecretString, - UDADetail, UtxoResponse, WatcherDetailResponse, WatcherRequest, WatcherResponse, - WatcherUtxoResponse, + RgbRemoveTransferRequest, RgbSaveTransferRequest, RgbTransferDetail, RgbTransferRequest, + RgbTransferResponse, RgbTransferStatusResponse, RgbTransfersResponse, SchemaDetail, + SchemasResponse, SecretString, TransferType, TxStatus, UDADetail, UtxoResponse, + WatcherDetailResponse, WatcherRequest, WatcherResponse, WatcherUtxoResponse, }, validators::RGBContext, }; use self::{ - carbonado::{retrieve_wallets, store_wallets}, + 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, + RGB_DEFAULT_NAME, STOCK_UNAVAILABLE, TRANSFER_UNAVAILABLE, }, contract::{export_contract, ExportContractError}, import::{import_contract, ImportContractError}, prefetch::{ prefetch_resolver_images, prefetch_resolver_import_rgb, prefetch_resolver_psbt, - prefetch_resolver_rgb, prefetch_resolver_utxo_status, prefetch_resolver_utxos, - prefetch_resolver_waddress, prefetch_resolver_wutxo, + prefetch_resolver_rgb, prefetch_resolver_txs_status, prefetch_resolver_utxo_status, + prefetch_resolver_utxos, prefetch_resolver_waddress, prefetch_resolver_wutxo, }, psbt::{fee_estimate, save_commit, CreatePsbtError}, - transfer::{AcceptTransferError, NewInvoiceError, NewPaymentError}, + structs::{AddressAmount, RgbTransfer}, + transfer::{extract_transfer, AcceptTransferError, NewInvoiceError, NewPaymentError}, wallet::{ - create_wallet, next_address, next_utxo, next_utxos, register_address, register_utxo, - sync_wallet, + create_wallet, get_address, next_address, next_utxo, next_utxos, register_address, + register_utxo, sync_wallet, }, }; @@ -522,6 +527,10 @@ pub enum TransferError { Accept(AcceptTransferError), /// Consignment cannot be encoded. WrongConsig(String), + /// Rgb Invoice cannot be decoded. {0} + WrongInvoice(String), + /// Bitcoin network be decoded. {0} + WrongNetwork(String), /// Occurs an error in export step. {0} Export(ExportContractError), } @@ -642,6 +651,15 @@ pub async fn transfer_asset( ) })?; + let mut rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) + .await + .map_err(|_| { + TransferError::Retrive( + CARBONADO_UNAVAILABLE.to_string(), + TRANSFER_UNAVAILABLE.to_string(), + ) + })?; + if rgb_account.wallets.get("default").is_none() { return Err(TransferError::NoWatcher); } @@ -652,7 +670,7 @@ pub async fn transfer_asset( terminal, } = request; let (psbt, transfer) = - pay_invoice(rgb_invoice, psbt, &mut stock).map_err(TransferError::Pay)?; + pay_invoice(rgb_invoice.clone(), psbt, &mut stock).map_err(TransferError::Pay)?; let commit = extract_commit(psbt.clone()).map_err(TransferError::Commitment)?; let wallet = rgb_account.wallets.get("default"); @@ -675,16 +693,44 @@ pub async fn transfer_asset( }; let consig = transfer - .to_strict_serialized::<{ usize::MAX }>() - .map_err(|err| TransferError::WrongConsig(err.to_string()))? - .to_hex(); + .to_strict_serialized::<{ U32 }>() + .map_err(|err| TransferError::WrongConsig(err.to_string()))?; + + let rgb_invoice = RgbInvoice::from_str(&rgb_invoice) + .map_err(|err| TransferError::WrongInvoice(err.to_string()))?; + + let bp_txid = bp::Txid::from_hex(&psbt.to_txid().to_hex()) + .map_err(|err| TransferError::WrongConsig(err.to_string()))?; + + let contract_id = rgb_invoice.contract.unwrap().to_string(); let consig_id = transfer.bindle_id().to_string(); + + let rgb_transfer = RgbTransfer { + consig_id: consig_id.clone(), + consig: consig.clone(), + tx: bp_txid, + is_send: true, + }; + + if let Some(transfers) = rgb_transfers.transfers.get(&contract_id) { + let mut new_transfer = transfers.to_owned(); + new_transfer.push(rgb_transfer); + rgb_transfers + .transfers + .insert(contract_id, new_transfer.to_vec()); + } else { + rgb_transfers + .transfers + .insert(contract_id, vec![rgb_transfer]); + } + + let consig_hex = consig.to_hex(); let commit = commit.to_hex(); let psbt = psbt.to_string(); let resp = RgbTransferResponse { consig_id, - consig, + consig: consig_hex, psbt, commit, }; @@ -696,6 +742,15 @@ pub async fn transfer_asset( ) })?; + store_transfers(sk, ASSETS_TRANSFERS, &rgb_transfers) + .await + .map_err(|_| { + TransferError::Write( + CARBONADO_UNAVAILABLE.to_string(), + STOCK_UNAVAILABLE.to_string(), + ) + })?; + Ok(resp) } @@ -751,7 +806,7 @@ pub async fn full_transfer_asset( ..Default::default() }; - if let TypedState::Amount(amount) = invoice.owned_state { + if let TypedState::Amount(target_amount) = invoice.owned_state { let contract = export_contract(contract_id, &mut stock, &mut resolver, &mut wallet) .map_err(TransferError::Export)?; @@ -761,7 +816,7 @@ pub async fn full_transfer_asset( .filter(|x| x.is_mine && !x.is_spent) .collect(); - let total: u64 = allocations + let asset_total: u64 = allocations .clone() .into_iter() .filter(|a| a.is_mine && !a.is_spent) @@ -771,14 +826,24 @@ pub async fn full_transfer_asset( }) .sum(); - if total < amount { + 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 FullRgbTransferRequest { + contract_id: _, + iface: _, + rgb_invoice, + descriptor, + change_terminal, + fee, + mut bitcoin_changes, + } = request; + let wildcard_terminal = "/*/*"; - let mut universal_desc = request.descriptor.to_string(); + let mut universal_desc = descriptor.to_string(); for contract_type in [ AssetType::RGB20, AssetType::RGB21, @@ -792,46 +857,85 @@ pub async fn full_transfer_asset( break; } } + let mut wallet = wallet.unwrap(); + let mut all_unspents = vec![]; // Get All Assets UTXOs - let mut total = 0; + let mut asset_total = 0; let mut asset_inputs = vec![]; + let mut asset_unspent_utxos = vec![]; + for contract_index in [AssetType::RGB20, AssetType::RGB21] { + let contract_index = contract_index as u32; + prefetch_resolver_utxo_status(contract_index, &mut wallet, &mut resolver).await; + sync_wallet(contract_index, &mut wallet, &mut resolver); + asset_unspent_utxos.append( + &mut next_utxos(contract_index, wallet.clone(), &mut resolver).map_err(|_| { + TransferError::Retrive( + "Esplora".to_string(), + "Retrieve Unspent UTXO unavaliable".to_string(), + ) + })?, + ) + } + + let mut rng = StdRng::seed_from_u64(0); + 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) => { - total += alloc_value; + if asset_total >= target_amount { + break; + } + asset_total += alloc_value; let input = PsbtInputRequest { descriptor: SecretString(universal_desc.clone()), - utxo: alloc.utxo, + utxo: alloc.utxo.clone(), utxo_terminal: alloc.derivation, tapret: None, }; asset_inputs.push(input); - - if amount <= total { - break; - } + total_asset_bitcoin_unspend += asset_unspent_utxos + .clone() + .into_iter() + .find(|x| x.outpoint.to_string() == alloc.utxo.clone()) + .map(|x| x.amount) + .unwrap_or_default(); } AllocationValue::UDA(_) => { let input = PsbtInputRequest { descriptor: SecretString(universal_desc.clone()), - utxo: alloc.utxo, + utxo: alloc.utxo.clone(), utxo_terminal: alloc.derivation, tapret: None, }; asset_inputs.push(input); + total_asset_bitcoin_unspend += asset_unspent_utxos + .clone() + .into_iter() + .find(|x| x.outpoint.to_string() == alloc.utxo.clone()) + .map(|x| x.amount) + .unwrap_or_default(); + 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) = request.fee { + if let PsbtFeeRequest::Value(fee_amount) = fee { let bitcoin_indexes = [0, 1]; - let mut wallet = wallet.unwrap(); - let mut all_unspents = vec![]; for bitcoin_index in bitcoin_indexes { prefetch_resolver_utxos( bitcoin_index, @@ -841,7 +945,6 @@ pub async fn full_transfer_asset( ) .await; prefetch_resolver_utxo_status(bitcoin_index, &mut wallet, &mut resolver).await; - sync_wallet(bitcoin_index, &mut wallet, &mut resolver); let mut unspent_utxos = next_utxos(bitcoin_index, wallet.clone(), &mut resolver) @@ -855,40 +958,61 @@ pub async fn full_transfer_asset( all_unspents.append(&mut unspent_utxos); } + let mut bitcoin_total = total_asset_bitcoin_unspend; for utxo in all_unspents { - 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, - }; - bitcoin_inputs.push(btc_input); - - if fee_amount < (total + utxo.amount) { + if bitcoin_total > (fee_amount + rnd_amount) { break; } else { - total += utxo.amount; + 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, + }; + + bitcoin_inputs.push(btc_input); } } + + if bitcoin_total < (fee_amount + rnd_amount) { + 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 { - bitcoin_inputs, - bitcoin_changes: vec![], - fee: request.fee, asset_inputs, + bitcoin_inputs, + bitcoin_changes, + fee, asset_descriptor_change: None, - asset_terminal_change: Some(request.change_terminal), + 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, - rgb_invoice: request.rgb_invoice, terminal: psbt_response.terminal, }, ) @@ -923,7 +1047,7 @@ pub async fn accept_transfer( STOCK_UNAVAILABLE.to_string(), ) })?; - // Prefetch + let mut resolver = ExplorerResolver { explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), ..Default::default() @@ -949,6 +1073,147 @@ pub async fn accept_transfer( Ok(resp) } +#[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] +#[display(doc_comments)] +pub enum SaveTransferError { + /// Some request data is missing. {0:?} + Validation(BTreeMap), + /// Retrieve I/O or connectivity error. {1} in {0} + Retrive(String, String), + /// Occurs an error in parse consig step. {0} + WrongConsig(AcceptTransferError), + /// Write I/O or connectivity error. {1} in {0} + Write(String, String), +} + +pub async fn save_transfer( + sk: &str, + request: RgbSaveTransferRequest, +) -> 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(SaveTransferError::Validation(errors)); + } + + let RgbSaveTransferRequest { + contract_id, + consignment, + } = request; + + let mut rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) + .await + .map_err(|_| { + SaveTransferError::Retrive( + CARBONADO_UNAVAILABLE.to_string(), + TRANSFER_UNAVAILABLE.to_string(), + ) + })?; + + let (txid, transfer) = extract_transfer(contract_id.clone(), consignment) + .map_err(SaveTransferError::WrongConsig)?; + + let consig = transfer + .to_strict_serialized::<{ U32 }>() + .map_err(|err| TransferError::WrongConsig(err.to_string())) + .map_err(|_| SaveTransferError::WrongConsig(AcceptTransferError::WrongHex))?; + + let consig_id = transfer.bindle_id().to_string(); + let rgb_transfer = RgbTransfer { + consig_id: consig_id.clone(), + consig: consig.clone(), + tx: txid, + is_send: false, + }; + + if let Some(transfers) = rgb_transfers.transfers.get(&contract_id.clone()) { + let mut new_transfer = transfers.to_owned(); + new_transfer.push(rgb_transfer); + rgb_transfers + .transfers + .insert(contract_id.clone(), new_transfer.to_vec()); + } else { + rgb_transfers + .transfers + .insert(contract_id.clone(), vec![rgb_transfer]); + } + + store_transfers(sk, ASSETS_TRANSFERS, &rgb_transfers) + .await + .map_err(|_| { + SaveTransferError::Write( + CARBONADO_UNAVAILABLE.to_string(), + STOCK_UNAVAILABLE.to_string(), + ) + })?; + + let mut status = BTreeMap::new(); + status.insert(consig_id, false); + + Ok(RgbTransferStatusResponse { + contract_id, + consig_status: status, + }) +} + +pub async fn remove_transfer( + sk: &str, + request: RgbRemoveTransferRequest, +) -> 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(SaveTransferError::Validation(errors)); + } + + let RgbRemoveTransferRequest { + contract_id, + consig_ids, + } = request; + + let mut rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS) + .await + .map_err(|_| { + SaveTransferError::Retrive( + CARBONADO_UNAVAILABLE.to_string(), + TRANSFER_UNAVAILABLE.to_string(), + ) + })?; + + if let Some(transfers) = rgb_transfers.transfers.get(&contract_id.clone()) { + let current_transfers = transfers + .clone() + .into_iter() + .filter(|x| !consig_ids.contains(&x.consig_id)) + .collect(); + + rgb_transfers + .transfers + .insert(contract_id.clone(), current_transfers); + } + + store_transfers(sk, ASSETS_TRANSFERS, &rgb_transfers) + .await + .map_err(|_| { + SaveTransferError::Write( + CARBONADO_UNAVAILABLE.to_string(), + STOCK_UNAVAILABLE.to_string(), + ) + })?; + + let status = consig_ids.into_iter().map(|x| (x, true)).collect(); + Ok(RgbTransferStatusResponse { + contract_id, + consig_status: status, + }) +} + 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?; @@ -1076,6 +1341,50 @@ pub async fn list_schemas(sk: &str) -> Result { Ok(SchemasResponse { schemas }) } +pub async fn list_transfers(sk: &str, contract_id: String) -> Result { + let rgb_transfers = retrieve_transfers(sk, ASSETS_TRANSFERS).await?; + + let mut resolver = ExplorerResolver { + explorer_url: BITCOIN_EXPLORER_API.read().await.to_string(), + ..Default::default() + }; + + let mut transfers = vec![]; + if let Some(transfer_activities) = rgb_transfers.transfers.get(&contract_id) { + let transfer_activities = transfer_activities.to_owned(); + 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 ty = if activity.is_send { + TransferType::Sended + } else { + TransferType::Received + }; + + 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 detail = RgbTransferDetail { + consig_id: activity.consig_id, + status, + ty, + }; + transfers.push(detail); + } + } + + Ok(RgbTransfersResponse { transfers }) +} + #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] #[display(doc_comments)] pub enum ImportError { diff --git a/src/rgb/carbonado.rs b/src/rgb/carbonado.rs index 1b4c3e16..17f59e10 100644 --- a/src/rgb/carbonado.rs +++ b/src/rgb/carbonado.rs @@ -4,6 +4,7 @@ use postcard::{from_bytes, to_allocvec}; use rgbstd::{persistence::Stock, stl::LIB_ID_RGB}; use strict_encoding::{StrictDeserialize, StrictSerialize}; +use crate::rgb::structs::RgbTransfers; use crate::{ carbonado::{retrieve, store}, rgb::{constants::RGB_STRICT_TYPE_VERSION, structs::RgbAccount}, @@ -123,3 +124,44 @@ pub async fn retrieve_wallets(sk: &str, name: &str) -> Result 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()) + .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 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()))?; + + if data.is_empty() { + Ok(RgbTransfers::default()) + } else { + let rgb_wallets = from_bytes(&data) + .map_err(|op| StorageError::StrictRetrive(name.to_string(), op.to_string()))?; + Ok(rgb_wallets) + } +} diff --git a/src/rgb/constants.rs b/src/rgb/constants.rs index 66821adc..f7b2123a 100644 --- a/src/rgb/constants.rs +++ b/src/rgb/constants.rs @@ -15,3 +15,4 @@ pub const CARBONADO_UNAVAILABLE: &str = "carbonado filesystem"; pub const CARBONADO_UNAVAILABLE: &str = "carbonado server"; pub const STOCK_UNAVAILABLE: &str = "Unable to access Stock data"; pub const WALLET_UNAVAILABLE: &str = "Unable to access Wallet data"; +pub const TRANSFER_UNAVAILABLE: &str = "Unable to access transfer data"; diff --git a/src/rgb/prefetch.rs b/src/rgb/prefetch.rs index 0d0fe747..e71425ef 100644 --- a/src/rgb/prefetch.rs +++ b/src/rgb/prefetch.rs @@ -1,7 +1,7 @@ #![allow(unused_imports)] #![allow(unused_variables)] use crate::rgb::resolvers::ExplorerResolver; -use crate::structs::AssetType; +use crate::structs::{AssetType, TxStatus}; use crate::{debug, structs::IssueMetaRequest}; use amplify::{ confinement::Confined, @@ -19,6 +19,7 @@ use bp::{LockTime, Outpoint, SeqNo, Tx, TxIn, TxOut, TxVer, Txid as BpTxid, VarI use rgb::{DeriveInfo, MiningStatus, RgbWallet, SpkDescriptor, Utxo}; use rgbstd::containers::Contract; use std::collections::HashMap; +use std::f32::consts::E; use std::{collections::BTreeMap, str::FromStr}; use strict_encoding::StrictDeserialize; use wallet::onchain::ResolveTx; @@ -294,7 +295,6 @@ pub async fn prefetch_resolver_utxos( limit: Option, ) { use std::collections::HashSet; - let esplora_client: EsploraBlockchain = EsploraBlockchain::new(&explorer.explorer_url, 1).with_concurrency(6); @@ -345,26 +345,23 @@ pub async fn prefetch_resolver_utxos( } related_txs.into_iter().for_each(|tx| { - let index = tx - .vout - .clone() - .into_iter() - .position(|txout| txout.scriptpubkey == script); - if let Some(index) = index { - let index = index; + for (index, vout) in tx.vout.iter().enumerate() { + if vout.scriptpubkey != script { + continue; + } let status = match tx.status.block_height { Some(height) => MiningStatus::Blockchain(height), _ => MiningStatus::Mempool, }; let outpoint = Outpoint::new( - rgbstd::Txid::from_str(&tx.txid.to_hex()).expect("invalid transactionID parse"), + bp::Txid::from_str(&tx.txid.to_hex()).expect("invalid outpoint parse"), index as u32, ); let new_utxo = Utxo { outpoint, status, - amount: tx.vout[index].value, + amount: vout.value, derivation: derive.clone(), }; utxos.insert(new_utxo); @@ -577,3 +574,30 @@ async fn retrieve_data(url: &str) -> Option> { None } + +pub async fn prefetch_resolver_txs_status(txids: Vec, explorer: &mut ExplorerResolver) { + let esplora_client = EsploraBlockchain::new(&explorer.explorer_url, 1).with_concurrency(6); + for txid in txids { + let tx_resp = esplora_client.get_tx_status(&txid).await; + if tx_resp.is_ok() { + let mut status = TxStatus::NotFound; + let tx_resp = tx_resp.unwrap_or_default(); + if let Some(tx_status) = tx_resp { + if tx_status.confirmed { + status = TxStatus::Block(tx_status.block_height.unwrap_or_default()); + } else { + status = TxStatus::Mempool; + } + } + explorer.txs_status.insert(txid, status); + } else { + let err = match tx_resp.err() { + Some(err) => err.to_string(), + None => "unknown explorer error".to_string(), + }; + + let err = TxStatus::Error(err); + explorer.txs_status.insert(txid, err); + } + } +} diff --git a/src/rgb/psbt.rs b/src/rgb/psbt.rs index feb8b66c..9c6d2e9a 100644 --- a/src/rgb/psbt.rs +++ b/src/rgb/psbt.rs @@ -169,17 +169,6 @@ pub fn create_psbt( .map(|AddressAmount { address, amount }| (address.script_pubkey().into(), amount)) .collect(); - // Define Tapret Proprierties - let proprietary_keys = vec![ProprietaryKeyDescriptor { - location: ProprietaryKeyLocation::Output(0_u16), - ty: ProprietaryKeyType { - prefix: RGB_PSBT_TAPRET.to_owned(), - subtype: 0, - }, - key: None, - value: None, - }]; - // Change Terminal Derivation let mut change_index = DerivationSubpath::new(); if let Some(terminal_change) = terminal_change { @@ -198,6 +187,17 @@ pub fn create_psbt( ) .map_err(|op| CreatePsbtError::Incomplete(op.to_string()))?; + // Define Tapret Proprierties + let proprietary_keys = vec![ProprietaryKeyDescriptor { + location: ProprietaryKeyLocation::Output((psbt.outputs.len() - 1) as u16), + ty: ProprietaryKeyType { + prefix: RGB_PSBT_TAPRET.to_owned(), + subtype: 0, + }, + key: None, + value: None, + }]; + for key in proprietary_keys { match key.location { ProprietaryKeyLocation::Input(pos) if pos as usize >= psbt.inputs.len() => { diff --git a/src/rgb/resolvers.rs b/src/rgb/resolvers.rs index b37e1dd1..48319ab1 100644 --- a/src/rgb/resolvers.rs +++ b/src/rgb/resolvers.rs @@ -18,6 +18,8 @@ use rgbstd::{ }; use wallet::onchain::{ResolveTx, TxResolverError}; +use crate::structs::TxStatus; + #[derive(Default)] pub struct ExplorerResolver { pub explorer_url: String, @@ -27,6 +29,7 @@ pub struct ExplorerResolver { pub txs: HashMap, pub bp_txs: HashMap, pub tx_height: HashMap, + pub txs_status: HashMap, } impl rgb::Resolver for ExplorerResolver { @@ -76,13 +79,11 @@ impl rgb::Resolver for ExplorerResolver { } related_txs.into_iter().for_each(|tx| { - let index = tx - .vout - .clone() - .into_iter() - .position(|txout| txout.scriptpubkey == script); - if let Some(index) = index { - let index = index; + for (index, vout) in tx.vout.iter().enumerate() { + if vout.scriptpubkey != script { + continue; + } + let status = match tx.status.block_height { Some(height) => MiningStatus::Blockchain(height), _ => MiningStatus::Mempool, @@ -94,7 +95,7 @@ impl rgb::Resolver for ExplorerResolver { let new_utxo = Utxo { outpoint, status, - amount: tx.vout[index].value, + amount: vout.value, derivation: derive.clone(), }; utxos.insert(new_utxo); @@ -264,3 +265,27 @@ impl ResolveSpent for ExplorerResolver { Ok(self.utxos_spent.contains(&outpoint)) } } + +#[derive(Clone, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum ResolverTxStatusError { + Unknown, +} + +pub trait ResolveTxStatus { + type Error: std::error::Error; + + fn resolve_tx_status(&mut self, txid: bitcoin::Txid) -> Result; +} + +impl ResolveTxStatus for ExplorerResolver { + type Error = ResolverTxStatusError; + + fn resolve_tx_status(&mut self, txid: bitcoin::Txid) -> Result { + if let Some(status) = self.txs_status.get(&txid) { + Ok(status.clone()) + } else { + Err(ResolverTxStatusError::Unknown) + } + } +} diff --git a/src/rgb/structs.rs b/src/rgb/structs.rs index 264af0b5..0075d3e7 100644 --- a/src/rgb/structs.rs +++ b/src/rgb/structs.rs @@ -1,7 +1,12 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; +use amplify::confinement::{Confined, U32}; use bitcoin::Address; use bitcoin_scripts::address::AddressCompat; +use bp::Txid; use rgb::{RgbWallet, TerminalPath}; use serde::{Deserialize, Serialize}; @@ -39,3 +44,21 @@ pub struct AddressTerminal { pub address: AddressCompat, pub terminal: TerminalPath, } + +#[derive( + Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Default, Display, +)] +#[display(doc_comments)] + +pub struct RgbTransfers { + pub transfers: BTreeMap>, +} + +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, Debug, Display)] +#[display("{tx}")] +pub struct RgbTransfer { + pub consig_id: String, + pub consig: Confined, 0, { U32 }>, + pub tx: Txid, + pub is_send: bool, +} diff --git a/src/rgb/transfer.rs b/src/rgb/transfer.rs index 58696ec5..54ceb10c 100644 --- a/src/rgb/transfer.rs +++ b/src/rgb/transfer.rs @@ -12,7 +12,7 @@ use rgbstd::{ interface::TypedState, persistence::{Inventory, Stash, Stock}, resolvers::ResolveHeight, - validation::{ResolveTx, Status}, + validation::{AnchoredBundle, ConsignmentApi, ResolveTx, Status}, }; use rgbwallet::{InventoryWallet, InvoiceParseError, RgbInvoice, RgbTransport}; use seals::txout::ExplicitSeal; @@ -57,6 +57,8 @@ pub enum NewPaymentError { pub enum AcceptTransferError { /// Consignment data have an invalid hexadecimal format. WrongHex, + /// ContractID cannot be decoded. {0} + WrongContract(String), /// Consignment cannot be decoded. {0} WrongConsig(String), /// The Consignment is invalid. Details: {0:?} @@ -200,3 +202,34 @@ where _ => Err(AcceptTransferError::Inconclusive), } } + +pub fn extract_transfer( + contract_id: String, + transfer: String, +) -> Result<(Txid, Bindle), AcceptTransferError> { + let serialized = Vec::::from_hex(&transfer).map_err(|_| AcceptTransferError::WrongHex)?; + let confined = Confined::try_from_iter(serialized.iter().copied()) + .map_err(|err| AcceptTransferError::WrongConsig(err.to_string()))?; + let transfer = Transfer::from_strict_serialized::<{ usize::MAX }>(confined) + .map_err(|err| AcceptTransferError::WrongConsig(err.to_string()))?; + + let contract_id = ContractId::from_str(&contract_id) + .map_err(|_| AcceptTransferError::WrongContract(contract_id))?; + for (bundle_id, _) in transfer.terminals() { + let Some(transitions) = transfer.known_transitions_by_bundle_id(bundle_id) else { + return Err(AcceptTransferError::Inconclusive); + }; + for transition in transitions { + if contract_id != transition.contract_id { + continue; + } + + if let Some(AnchoredBundle { anchor, bundle: _ }) = transfer.anchored_bundle(bundle_id) + { + return Ok((anchor.txid, Bindle::new(transfer))); + } + } + } + + Err(AcceptTransferError::Inconclusive) +} diff --git a/src/rgb/wallet.rs b/src/rgb/wallet.rs index ae2acbbf..70928cac 100644 --- a/src/rgb/wallet.rs +++ b/src/rgb/wallet.rs @@ -80,6 +80,29 @@ pub fn list_utxos(wallet: RgbWallet) -> Result, anyhow::Error> { Ok(wallet.utxos.into_iter().collect()) } +pub fn get_address( + iface_index: u32, + index: u32, + wallet: RgbWallet, + network: AddressNetwork, +) -> Result { + let scripts = wallet.descr.derive(iface_index, 0..index); + let addresses: Vec = scripts + .into_iter() + .map(|(d, sb)| { + let sc = Script::from_str(&sb.to_hex_string()).expect("invalid script data"); + let address = + AddressCompat::from_script(&sc.into(), network).expect("invalid address data"); + let terminal = d.terminal; + AddressTerminal { address, terminal } + }) + .collect(); + + debug!(format!("RGB Addresses: {addresses:?}")); + + Ok(addresses[addresses.len() - 1].clone()) +} + pub fn next_address( iface_index: u32, wallet: RgbWallet, diff --git a/src/structs.rs b/src/structs.rs index 556ad229..6fd15e8d 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,6 +1,6 @@ use garde::Validate; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use zeroize::{Zeroize, ZeroizeOnDrop}; pub use bdk::{Balance, BlockTime, TransactionDetails}; @@ -587,15 +587,18 @@ pub struct FullRgbTransferRequest { #[garde(ascii)] #[garde(length(min = 0, max = 512))] pub rgb_invoice: String, - /// Asset or Bitcoin Descriptor + /// Asset Descriptor #[garde(custom(is_descriptor))] pub descriptor: SecretString, - /// Bitcoin Terminal Change + /// Asset Terminal Change #[garde(ascii)] pub change_terminal: String, /// Bitcoin Fee #[garde(dive)] pub fee: PsbtFeeRequest, + /// Bitcoin Change Addresses (format: {address}:{amount}) + #[garde(length(min = 0, max = 999))] + pub bitcoin_changes: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -619,6 +622,9 @@ pub struct SelfFullRgbTransferRequest { #[garde(ascii)] #[garde(length(min = 4, max = 4))] pub terminal: String, + /// Bitcoin Change Addresses (format: {address}:{amount}) + #[garde(length(min = 0, max = 999))] + pub bitcoin_changes: Vec, /// Bitcoin Fee #[garde(skip)] pub fee: Option, @@ -662,6 +668,46 @@ pub struct AcceptResponse { pub valid: bool, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[derive(Validate)] +#[garde(context(RGBContext))] +pub struct RgbSaveTransferRequest { + /// Contract ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub contract_id: String, + + /// Consignment encoded in hexadecimal + #[garde(ascii)] + #[garde(length(min = 0, max = U64))] + pub consignment: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[derive(Validate)] +#[garde(context(RGBContext))] +pub struct RgbRemoveTransferRequest { + /// Contract ID + #[garde(ascii)] + #[garde(length(min = 0, max = 100))] + pub contract_id: String, + + /// Consignment ID + #[garde(length(min = 1, max = 999))] + pub consig_ids: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RgbTransferStatusResponse { + /// Contract ID + pub contract_id: String, + /// Transfer ID + pub consig_status: BTreeMap, +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ContractsResponse { @@ -876,6 +922,48 @@ pub struct FileMetadata { pub metadata: [u8; 8], } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RgbTransfersResponse { + /// List of avaliable transfers + pub transfers: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RgbTransferDetail { + pub consig_id: String, + pub status: TxStatus, + #[serde(rename = "type")] + pub ty: TransferType, +} + +#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Debug, Clone, Display)] +#[serde(rename_all = "camelCase")] +pub enum TxStatus { + #[display(inner)] + #[serde(rename = "not_found")] + NotFound, + #[serde(rename = "error")] + Error(String), + #[serde(rename = "mempool")] + Mempool, + #[serde(rename = "block")] + Block(u32), +} + +#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Debug, Clone, Display)] +#[serde(rename_all = "camelCase")] +pub enum TransferType { + #[display(inner)] + #[serde(rename = "sended")] + Sended, + #[serde(rename = "received")] + Received, + #[serde(rename = "unknown")] + Unknown, +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct RgbInvoiceResponse { diff --git a/src/web.rs b/src/web.rs index 683f2058..7e27dab6 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,6 +1,7 @@ use crate::structs::{ AcceptRequest, FullRgbTransferRequest, ImportRequest, InvoiceRequest, IssueRequest, - PsbtRequest, ReIssueRequest, RgbTransferRequest, SecretString, SignPsbtRequest, WatcherRequest, + PsbtRequest, ReIssueRequest, RgbRemoveTransferRequest, RgbSaveTransferRequest, + RgbTransferRequest, SecretString, SignPsbtRequest, WatcherRequest, }; // use crate::{carbonado, lightning, rgb}; @@ -624,6 +625,49 @@ pub mod rgb { }) } + #[wasm_bindgen] + pub fn list_transfers(nostr_hex_sk: String, contract_id: String) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + match crate::rgb::list_transfers(&nostr_hex_sk, contract_id).await { + Ok(result) => Ok(JsValue::from_string( + serde_json::to_string(&result).unwrap(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } + + #[wasm_bindgen] + pub fn save_transfer(nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let req: RgbSaveTransferRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::save_transfer(&nostr_hex_sk, req).await { + Ok(result) => Ok(JsValue::from_string( + serde_json::to_string(&result).unwrap(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } + + #[wasm_bindgen] + pub fn remove_transfer(nostr_hex_sk: String, request: JsValue) -> Promise { + set_panic_hook(); + + future_to_promise(async move { + let req: RgbRemoveTransferRequest = serde_wasm_bindgen::from_value(request).unwrap(); + match crate::rgb::remove_transfer(&nostr_hex_sk, req).await { + Ok(result) => Ok(JsValue::from_string( + serde_json::to_string(&result).unwrap(), + )), + Err(err) => Err(JsValue::from_string(err.to_string())), + } + }) + } #[wasm_bindgen] pub fn decode_invoice(invoice: String) -> Promise { set_panic_hook(); diff --git a/tests/rgb.rs b/tests/rgb.rs index b51dc6c3..72153489 100644 --- a/tests/rgb.rs +++ b/tests/rgb.rs @@ -14,6 +14,7 @@ mod rgb { // mod collectibles; mod collectibles; + mod consig; mod drain; mod dustless; mod fungibles; diff --git a/tests/rgb/integration/consig.rs b/tests/rgb/integration/consig.rs new file mode 100644 index 00000000..c475b0c7 --- /dev/null +++ b/tests/rgb/integration/consig.rs @@ -0,0 +1,175 @@ +#![cfg(not(target_arch = "wasm32"))] +use anyhow::Result; +use bitmask_core::{ + bitcoin::{save_mnemonic, sign_psbt_file}, + rgb::{create_watcher, list_transfers, remove_transfer, save_transfer, watcher_next_address}, + structs::{ + DecryptedWalletData, RgbRemoveTransferRequest, RgbSaveTransferRequest, SecretString, + SignPsbtRequest, TransferType, TxStatus, WatcherRequest, + }, +}; + +use crate::rgb::integration::utils::{ + create_new_invoice, create_new_psbt, create_new_transfer, issuer_issue_contract_v2, + send_some_coins, UtxoFilter, ISSUER_MNEMONIC, OWNER_MNEMONIC, +}; + +#[tokio::test] +pub async fn allow_save_read_remove_transfers() -> Result<()> { + // 0. Retrieve all keys + let issuer_keys: DecryptedWalletData = 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?; + + // 1. Create All Watchers + let watcher_name = "default"; + let issuer_sk = issuer_keys.private.nostr_prv.to_string(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: issuer_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&issuer_sk, create_watch_req.clone()).await?; + + let owner_sk = owner_keys.private.nostr_prv.to_string(); + let create_watch_req = WatcherRequest { + name: watcher_name.to_string(), + xpub: owner_keys.public.watcher_xpub.clone(), + force: true, + }; + create_watcher(&owner_sk, create_watch_req.clone()).await?; + + // 2. Issuer Contract + let issuer_resp = issuer_issue_contract_v2( + 1, + "RGB20", + 5, + false, + true, + None, + Some("0.1".to_string()), + Some(UtxoFilter::with_amount_equal_than(10000000)), + ) + .await?; + let issuer_resp = &issuer_resp[0]; + + // 3. Owner Create Invoice + let owner_invoice = &create_new_invoice( + &issuer_resp.contract_id, + &issuer_resp.iface, + 2, + owner_keys.clone(), + None, + Some(issuer_resp.clone().contract.strict), + ) + .await?; + + // 4. Create First Transfer + let psbt_resp = create_new_psbt( + &issuer_resp.contract_id, + &issuer_resp.iface, + vec![issuer_resp.issue_utxo.clone()], + issuer_keys.clone(), + ) + .await?; + let transfer_resp = &create_new_transfer( + issuer_keys.clone(), + owner_invoice.clone(), + psbt_resp.clone(), + ) + .await?; + + let request = SignPsbtRequest { + psbt: transfer_resp.psbt.clone(), + descriptors: vec![SecretString( + issuer_keys.private.rgb_assets_descriptor_xprv.clone(), + )] + .to_vec(), + }; + let resp = sign_psbt_file(request).await; + assert!(resp.is_ok()); + + // 5. Save Consig (Owner Side) + let transfer = transfer_resp.clone(); + let all_sks = [owner_sk.clone()]; + for sk in all_sks { + let request = RgbSaveTransferRequest { + contract_id: issuer_resp.contract_id.clone(), + consignment: transfer.consig.clone(), + }; + let save_resp = save_transfer(&sk, request).await; + assert!(save_resp.is_ok()); + } + + // 6. Check Consig Status (Both Sides) + let contract_id = issuer_resp.contract_id.clone(); + let all_sks = [issuer_sk.clone(), owner_sk.clone()]; + for sk in all_sks { + let is_issuer = sk == issuer_sk; + + let list_resp = list_transfers(&sk, contract_id.clone()).await; + assert!(list_resp.is_ok()); + + let list_resp = list_resp?; + if let Some(consig_status) = list_resp + .transfers + .into_iter() + .find(|x| x.consig_id == transfer.consig_id) + { + matches!(consig_status.status, TxStatus::Mempool); + + if is_issuer { + assert_eq!(consig_status.ty, TransferType::Sended); + } else { + assert_eq!(consig_status.ty, TransferType::Received); + } + } + } + + // 7. Check Consig Status After Block (Both Sides) + let address = watcher_next_address(&owner_sk, watcher_name, "RGB20").await?; + send_some_coins(&address.address, "0.1").await; + + let contract_id = issuer_resp.contract_id.clone(); + let all_sks = [issuer_sk.clone(), owner_sk.clone()]; + for sk in all_sks { + let list_resp = list_transfers(&sk, contract_id.clone()).await; + assert!(list_resp.is_ok()); + + let list_resp = list_resp?; + if let Some(consig_status) = list_resp + .transfers + .into_iter() + .find(|x| x.consig_id == transfer.consig_id) + { + matches!(consig_status.status, TxStatus::Block(_)); + } + } + + // 8. Remove Consig (Both Sides) + let contract_id = issuer_resp.contract_id.clone(); + let all_sks = [issuer_sk.clone(), owner_sk.clone()]; + for sk in all_sks { + let req = RgbRemoveTransferRequest { + contract_id: contract_id.clone(), + consig_ids: vec![transfer.consig_id.clone()], + }; + let list_resp = remove_transfer(&sk, req).await; + assert!(list_resp.is_ok()); + + let list_resp = list_transfers(&sk, contract_id.clone()).await; + assert!(list_resp.is_ok()); + + let list_resp = list_resp?; + assert_eq!(list_resp.transfers.len(), 0); + } + + Ok(()) +} diff --git a/tests/rgb/integration/internal.rs b/tests/rgb/integration/internal.rs index ed69d1e8..099eeffb 100644 --- a/tests/rgb/integration/internal.rs +++ b/tests/rgb/integration/internal.rs @@ -68,8 +68,9 @@ async fn allow_fungible_full_transfer_op() -> anyhow::Result<()> { iface: issuer_resp.iface, rgb_invoice: owner_resp.invoice.to_string(), descriptor: SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.to_string()), - change_terminal: "/1/0".to_string(), - fee: PsbtFeeRequest::Value(546), + change_terminal: "/20/1".to_string(), + fee: PsbtFeeRequest::Value(1000), + bitcoin_changes: vec![], }; let issue_sk = issuer_keys.private.nostr_prv.to_string(); @@ -136,8 +137,9 @@ async fn allow_uda_full_transfer_op() -> anyhow::Result<()> { iface: issuer_resp.iface, rgb_invoice: owner_resp.invoice.to_string(), descriptor: SecretString(issuer_keys.public.rgb_udas_descriptor_xpub.to_string()), - change_terminal: "/1/0".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();