diff --git a/Cargo.lock b/Cargo.lock index 8bc4c86ea7..1f70961816 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3371,6 +3371,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "workflow-core", + "workflow-http", "workflow-log", "workflow-node", "workflow-rpc", diff --git a/cli/src/extensions/transaction.rs b/cli/src/extensions/transaction.rs index 415aa7a34b..1877a2fcae 100644 --- a/cli/src/extensions/transaction.rs +++ b/cli/src/extensions/transaction.rs @@ -15,6 +15,7 @@ impl TransactionTypeExtension for TransactionKind { match self { TransactionKind::Incoming => style(s).green().to_string(), TransactionKind::Outgoing => style(s).red().to_string(), + TransactionKind::Meta => style(s).dim().to_string(), TransactionKind::External => style(s).red().to_string(), TransactionKind::Batch => style(s).dim().to_string(), TransactionKind::Reorg => style(s).dim().to_string(), @@ -153,6 +154,7 @@ impl TransactionExtension for TransactionRecord { } } TransactionData::Outgoing { fees, aggregate_input_value, transaction, payment_value, change_value, .. } + | TransactionData::Meta { fees, aggregate_input_value, transaction, payment_value, change_value, .. } | TransactionData::Batch { fees, aggregate_input_value, transaction, payment_value, change_value, .. } | TransactionData::TransferIncoming { fees, aggregate_input_value, transaction, payment_value, change_value, .. } | TransactionData::TransferOutgoing { fees, aggregate_input_value, transaction, payment_value, change_value, .. } => { diff --git a/cli/src/modules/pskb.rs b/cli/src/modules/pskb.rs index 3757f939ac..018a597a4f 100644 --- a/cli/src/modules/pskb.rs +++ b/cli/src/modules/pskb.rs @@ -192,7 +192,7 @@ impl Pskb { } let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?; let account = ctx.wallet().account()?; - match account.pskb_broadcast(&pskb).await { + match account.pskb_broadcast(&pskb, false).await { Ok(sent) => tprintln!(ctx, "Sent transactions {:?}", sent), Err(e) => terrorln!(ctx, "Send error {:?}", e), } diff --git a/wallet/core/Cargo.toml b/wallet/core/Cargo.toml index 3e057b6a77..8535248521 100644 --- a/wallet/core/Cargo.toml +++ b/wallet/core/Cargo.toml @@ -99,6 +99,7 @@ workflow-node.workspace = true workflow-rpc.workspace = true workflow-store.workspace = true workflow-wasm.workspace = true +workflow-http.workspace = true xxhash-rust.workspace = true zeroize.workspace = true indexed_db_futures.workspace = true diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 31aee5da15..987d4d144f 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -334,7 +334,7 @@ pub trait Account: AnySync + Send + Sync + 'static { let mut ids = vec![]; while let Some(transaction) = stream.try_next().await? { transaction.try_sign()?; - ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + ids.push(transaction.try_submit(&self.wallet().rpc_api(), false).await?); if let Some(notifier) = notifier.as_ref() { notifier(&transaction); @@ -370,7 +370,7 @@ pub trait Account: AnySync + Send + Sync + 'static { let mut ids = vec![]; while let Some(transaction) = stream.try_next().await? { transaction.try_sign()?; - ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + ids.push(transaction.try_submit(&self.wallet().rpc_api(), false).await?); if let Some(notifier) = notifier.as_ref() { notifier(&transaction); @@ -476,7 +476,7 @@ pub trait Account: AnySync + Send + Sync + 'static { } } - async fn pskb_broadcast(self: Arc, bundle: &Bundle) -> Result, Error> { + async fn pskb_broadcast(self: Arc, bundle: &Bundle, is_meta_tx: bool) -> Result, Error> { let mut ids = Vec::new(); let mut stream = bundle_to_finalizer_stream(bundle); @@ -484,8 +484,9 @@ pub trait Account: AnySync + Send + Sync + 'static { match result { Ok(pskt) => { let change = self.change_address()?; - let transaction = pskt_to_pending_transaction(pskt, self.wallet().network_id()?, change)?; - ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + let transaction = + pskt_to_pending_transaction(pskt, self.wallet().network_id()?, change, self.utxo_context().clone().into())?; + ids.push(transaction.try_submit(&self.wallet().rpc_api(), is_meta_tx).await?); } Err(e) => { eprintln!("Error processing a PSKT from bundle: {:?}", e); @@ -536,7 +537,7 @@ pub trait Account: AnySync + Send + Sync + 'static { let mut ids = vec![]; while let Some(transaction) = stream.try_next().await? { transaction.try_sign()?; - ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + ids.push(transaction.try_submit(&self.wallet().rpc_api(), false).await?); if let Some(notifier) = notifier.as_ref() { notifier(&transaction); @@ -714,7 +715,7 @@ pub trait DerivationCapableAccount: Account { let mut stream = generator.stream(); while let Some(transaction) = stream.try_next().await? { transaction.try_sign_with_keys(&keys, None)?; - let id = transaction.try_submit(&rpc).await?; + let id = transaction.try_submit(&rpc, false).await?; if let Some(notifier) = notifier { notifier(index, aggregate_utxo_count, balance, Some(id)); } diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index 23065a959c..67b42eb1a5 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -292,6 +292,7 @@ pub fn pskt_to_pending_transaction( finalized_pskt: PSKT, network_id: NetworkId, change_address: Address, + source_utxo_context: Option, ) -> Result { let mass = 10; let (signed_tx, _) = match finalized_pskt.clone().extractor() { @@ -341,7 +342,7 @@ pub fn pskt_to_pending_transaction( change_address, utxo_iterator, priority_utxo_entries: None, - source_utxo_context: None, + source_utxo_context, destination_utxo_context: None, fee_rate: None, final_transaction_priority_fee: fee_u.into(), @@ -491,6 +492,7 @@ pub async fn commit_reveal_batch_bundle( pskt_finalizer.clone(), network_id, account.clone().as_derivation_capable()?.change_address()?, + account.utxo_context().clone().into(), ) { Ok(tx) => Ok(tx.id()), Err(e) => Err(e), diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index 12b546723d..98fc4b5862 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -603,14 +603,15 @@ pub struct FeeRatePollerDisableResponse {} pub struct TransactionsDataGetRequest { pub account_id: AccountId, pub network_id: NetworkId, - pub filter: Option>, + pub kind_filter: Option>, + pub group_filter: Option>, pub start: u64, pub end: u64, } impl TransactionsDataGetRequest { pub fn with_range(account_id: AccountId, network_id: NetworkId, range: std::ops::Range) -> Self { - Self { account_id, network_id, filter: None, start: range.start, end: range.end } + Self { account_id, network_id, kind_filter: None, group_filter: None, start: range.start, end: range.end } } } diff --git a/wallet/core/src/storage/interface.rs b/wallet/core/src/storage/interface.rs index bc756a7235..c039c6f6c7 100644 --- a/wallet/core/src/storage/interface.rs +++ b/wallet/core/src/storage/interface.rs @@ -124,7 +124,8 @@ pub trait TransactionRecordStore: Send + Sync { &self, binding: &Binding, network_id: &NetworkId, - filter: Option>, + kind_filter: Option>, + group_filter: Option>, range: std::ops::Range, ) -> Result; diff --git a/wallet/core/src/storage/local/transaction/fsio.rs b/wallet/core/src/storage/local/transaction/fsio.rs index a57a440112..8c9f796101 100644 --- a/wallet/core/src/storage/local/transaction/fsio.rs +++ b/wallet/core/src/storage/local/transaction/fsio.rs @@ -136,14 +136,37 @@ impl TransactionRecordStore for TransactionStore { &self, binding: &Binding, network_id: &NetworkId, - filter: Option>, + kind_filter: Option>, + group_filter: Option>, range: std::ops::Range, ) -> Result { let folder = self.ensure_folder(binding, network_id).await?; let ids = self.enumerate(binding, network_id).await?; let mut transactions = vec![]; + let total = if let Some(group_filter) = group_filter { + let mut located = 0; + + for id in ids { + let path = folder.join(id.to_hex()); + + match read(&path, None).await { + Ok(tx) => { + if group_filter.contains(&tx.kind().into()) { + if located >= range.start && located < range.end { + transactions.push(Arc::new(tx)); + } - let total = if let Some(filter) = filter { + located += 1; + } + } + Err(err) => { + log_error!("Error loading transaction {id}: {:?}", err); + } + } + } + + located + } else if let Some(filter) = kind_filter { let mut located = 0; for id in ids { diff --git a/wallet/core/src/storage/local/transaction/indexdb.rs b/wallet/core/src/storage/local/transaction/indexdb.rs index 463508f611..f0c675f322 100644 --- a/wallet/core/src/storage/local/transaction/indexdb.rs +++ b/wallet/core/src/storage/local/transaction/indexdb.rs @@ -6,7 +6,7 @@ use crate::imports::*; use crate::result::Result; use crate::storage::interface::{StorageStream, TransactionRangeResult}; use crate::storage::TransactionRecord; -use crate::storage::{Binding, TransactionKind, TransactionRecordStore}; +use crate::storage::{Binding, TransactionGroup, TransactionKind, TransactionRecordStore}; use indexed_db_futures::prelude::*; use itertools::Itertools; use js_sys::{Date, Uint8Array}; @@ -15,6 +15,8 @@ use workflow_core::task::call_async_no_send; const TRANSACTIONS_STORE_NAME: &str = "transactions"; const TRANSACTIONS_STORE_ID_INDEX: &str = "id"; const TRANSACTIONS_STORE_TIMESTAMP_INDEX: &str = "timestamp"; +const TRANSACTIONS_STORE_GROUP_INDEX: &str = "group"; +const TRANSACTIONS_STORE_KIND_INDEX: &str = "kind"; const TRANSACTIONS_STORE_DATA_INDEX: &str = "data"; const ENCRYPTION_KIND: EncryptionKind = EncryptionKind::XChaCha20Poly1305; @@ -26,10 +28,10 @@ pub struct Inner { impl Inner { async fn open_db(&self, db_name: String) -> Result { call_async_no_send!(async move { - let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&db_name, 2) + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&db_name, 3) .map_err(|err| Error::Custom(format!("Failed to open indexdb database {:?}", err)))?; - let fix_timestamp = Arc::new(Mutex::new(false)); - let fix_timestamp_clone = fix_timestamp.clone(); + let upgrade_needed = Arc::new(Mutex::new(false)); + let upgrade_needed_clone = upgrade_needed.clone(); let on_upgrade_needed = move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { let old_version = evt.old_version(); if old_version < 1.0 { @@ -51,10 +53,24 @@ impl Inner { &IdbKeyPath::str(TRANSACTIONS_STORE_DATA_INDEX), &db_index_params, )?; + } + if old_version < 3.0 { + let tx = evt.transaction(); + let object_store = tx.object_store(TRANSACTIONS_STORE_NAME)?; - // these changes are not required for new db - } else if old_version < 2.0 { - *fix_timestamp_clone.lock().unwrap() = true; + let db_index_params = IdbIndexParameters::new(); + db_index_params.set_unique(false); + object_store.create_index_with_params( + TRANSACTIONS_STORE_GROUP_INDEX, + &IdbKeyPath::str(TRANSACTIONS_STORE_GROUP_INDEX), + &db_index_params, + )?; + object_store.create_index_with_params( + TRANSACTIONS_STORE_KIND_INDEX, + &IdbKeyPath::str(TRANSACTIONS_STORE_KIND_INDEX), + &db_index_params, + )?; + *upgrade_needed_clone.lock().unwrap() = true; } // // Check if the object store exists; create it if it doesn't // if !evt.db().object_store_names().any(|n| n == TRANSACTIONS_STORE_NAME) { @@ -68,8 +84,8 @@ impl Inner { let db = db_req.await.map_err(|err| Error::Custom(format!("Open database request failed for indexdb database {:?}", err)))?; - if *fix_timestamp.lock().unwrap() { - log_info!("DEBUG: fixing timestamp"); + if *upgrade_needed.lock().unwrap() { + log_info!("DEBUG: upgrading database {:?} records", db_name); let idb_tx = db .transaction_on_one_with_mode(TRANSACTIONS_STORE_NAME, IdbTransactionMode::Readwrite) .map_err(|err| Error::Custom(format!("Failed to open indexdb transaction for reading {:?}", err)))?; @@ -92,17 +108,15 @@ impl Inner { loop { let js_value = cursor.value(); if let Ok(record) = transaction_record_from_js_value(&js_value, None) { - if record.unixtime_msec.is_some() { - let new_js_value = transaction_record_to_js_value(&record, None, ENCRYPTION_KIND)?; + let new_js_value = transaction_record_to_js_value(&record, None, ENCRYPTION_KIND)?; - //log_info!("DEBUG: new_js_value: {:?}", new_js_value); + //log_info!("DEBUG: new_js_value: {:?}", new_js_value); - cursor - .update(&new_js_value) - .map_err(|err| Error::Custom(format!("Failed to update record timestamp {:?}", err)))? - .await - .map_err(|err| Error::Custom(format!("Failed to update record timestamp {:?}", err)))?; - } + cursor + .update(&new_js_value) + .map_err(|err| Error::Custom(format!("Failed to update record timestamp {:?}", err)))? + .await + .map_err(|err| Error::Custom(format!("Failed to update record timestamp {:?}", err)))?; } if let Ok(b) = cursor.continue_cursor() { match b.await { @@ -286,7 +300,8 @@ impl TransactionRecordStore for TransactionStore { &self, binding: &Binding, network_id: &NetworkId, - _filter: Option>, + kind_filter: Option>, + group_filter: Option>, range: std::ops::Range, ) -> Result { log_info!("DEBUG IDB: Loading transaction records for range {:?}", range); @@ -302,11 +317,100 @@ impl TransactionRecordStore for TransactionStore { let store = idb_tx .object_store(TRANSACTIONS_STORE_NAME) .map_err(|err| Error::Custom(format!("Failed to open indexdb object store for reading {:?}", err)))?; - let total = store - .count() - .map_err(|err| Error::Custom(format!("Failed to count indexdb records {:?}", err)))? - .await - .map_err(|err| Error::Custom(format!("Failed to count indexdb records from future {:?}", err)))?; + + let mut groups = vec![]; + let mut kinds = vec![]; + let total = if let Some(group_filter) = group_filter { + let mut total = 0; + for group in group_filter { + groups.push(group.to_string()); + let key_range = web_sys::IdbKeyRange::only(&JsValue::from(group.to_string())) + .map_err(|err| Error::Custom(format!("Failed to create key range {:?}", err)))?; + let count = store + .index(TRANSACTIONS_STORE_GROUP_INDEX) + .map_err(|err| Error::Custom(format!("Failed to get group index {:?}", err)))? + .count_with_key(&key_range) + .map_err(|err| Error::Custom(format!("Failed to count records {:?}", err)))? + .await + .map_err(|err| Error::Custom(format!("Failed to get count from future {:?}", err)))?; + total += count; + } + total + } else if let Some(kind_filter) = kind_filter { + let mut total = 0; + for kind in kind_filter { + kinds.push(kind.to_string()); + let key_range = web_sys::IdbKeyRange::only(&JsValue::from(kind.to_string())) + .map_err(|err| Error::Custom(format!("Failed to create key range {:?}", err)))?; + let count = store + .index(TRANSACTIONS_STORE_KIND_INDEX) + .map_err(|err| Error::Custom(format!("Failed to get kind index {:?}", err)))? + .count_with_key(&key_range) + .map_err(|err| Error::Custom(format!("Failed to count records {:?}", err)))? + .await + .map_err(|err| Error::Custom(format!("Failed to get count from future {:?}", err)))?; + total += count; + } + total + } else { + store + .count() + .map_err(|err| Error::Custom(format!("Failed to count indexdb records {:?}", err)))? + .await + .map_err(|err| Error::Custom(format!("Failed to count indexdb records from future {:?}", err)))? + }; + + // let binding = if let Some(group_filter) = &group_filter { + // store + // .index(TRANSACTIONS_STORE_GROUP_INDEX) + // .map_err(|err| Error::Custom(format!("Failed to open indexdb group index {:?}", err)))? + // } else if let Some(kind_filter) = &kind_filter { + // store + // .index(TRANSACTIONS_STORE_KIND_INDEX) + // .map_err(|err| Error::Custom(format!("Failed to open indexdb kind index {:?}", err)))? + // } else { + // store + // .index(TRANSACTIONS_STORE_TIMESTAMP_INDEX) + // .map_err(|err| Error::Custom(format!("Failed to open indexdb timestamp index {:?}", err)))? + // }; + + // let mut cursors = vec![]; + // if let Some(group_filter) = &group_filter { + // for group in group_filter { + // let key_range = web_sys::IdbKeyRange::only(&JsValue::from(group.to_string())) + // .map_err(|err| Error::Custom(format!("Failed to create key range {:?}", err)))?; + // let cursor = binding + // .open_cursor_with_range_and_direction(&key_range, web_sys::IdbCursorDirection::Prev) + // .map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor for reading {:?}", err)))?; + // cursors.push((cursor, group.to_string())); + // } + // } else if let Some(kind_filter) = &kind_filter { + // for kind in kind_filter { + // let key_range = web_sys::IdbKeyRange::only(&JsValue::from(kind.to_string())) + // .map_err(|err| Error::Custom(format!("Failed to create key range {:?}", err)))?; + // let cursor = binding + // .open_cursor_with_range_and_direction(&key_range, web_sys::IdbCursorDirection::Prev) + // .map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor for reading {:?}", err)))?; + // cursors.push((cursor, kind.to_string())); + // } + // } else { + // let cursor = binding + // .open_cursor_with_range_and_direction(&JsValue::NULL, web_sys::IdbCursorDirection::Prev) + // .map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor for reading {:?}", err)))?; + // cursors.push((cursor, String::new())); + // } + + // // Initialize cursors and collect first values + // let mut cursor_values = vec![]; + // for (cursor, filter_value) in cursors { + // if let Some(cursor) = cursor.await.map_err(|err| Error::Custom(format!("Failed to open cursor {:?}", err)))? { + // let timestamp = cursor.key().as_f64().unwrap_or(0.0); + // cursor_values.push((cursor, timestamp, filter_value)); + // } + // } + + // // Sort by timestamp in descending order + // cursor_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); let binding = store .index(TRANSACTIONS_STORE_TIMESTAMP_INDEX) @@ -314,53 +418,129 @@ impl TransactionRecordStore for TransactionStore { let cursor = binding .open_cursor_with_range_and_direction(&JsValue::NULL, web_sys::IdbCursorDirection::Prev) .map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor for reading {:?}", err)))?; - let mut records = vec![]; let cursor = cursor.await.map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor {:?}", err)))?; + + let mut transactions = vec![]; + let count = range.end - range.start; if let Some(cursor) = cursor { - if range.start > 0 { - let res = cursor - .advance(range.start as u32) - .map_err(|err| Error::Custom(format!("Unable to advance indexdb cursor {:?}", err)))? - .await; - let _res = res.map_err(|err| Error::Custom(format!("Unable to advance indexdb cursor future {:?}", err)))?; - // if !res { - // //return Err(Error::Custom(format!("Unable to advance indexdb cursor future {:?}", err))); - // } - } - let count = range.end - range.start; - loop { - if records.len() < count { - records.push(cursor.value()); - if let Ok(b) = cursor.continue_cursor() { - match b.await { - Ok(b) => { - if !b { + if !groups.is_empty() { + let mut located = 0; + loop { + if transactions.len() < count { + if located >= range.start && located < range.end { + match transaction_record_from_js_value(&cursor.value(), None) { + Ok(transaction_record) => { + if groups.contains(&TransactionGroup::from(transaction_record.kind()).to_string()) { + transactions.push(Arc::new(transaction_record)) + } + } + Err(err) => { + log_error!("Failed to deserialize transaction record from indexdb {:?}", err); + } + } + } + + located += 1; + + if let Ok(b) = cursor.continue_cursor() { + match b.await { + Ok(b) => { + if !b { + break; + } + } + Err(err) => { + log_info!("DEBUG IDB: Loading transaction error, cursor.continue_cursor() {:?}", err); break; } } + } else { + break; + } + } else { + break; + } + } + } else if !kinds.is_empty() { + let mut located = 0; + loop { + if transactions.len() < count { + if located >= range.start && located < range.end { + match transaction_record_from_js_value(&cursor.value(), None) { + Ok(transaction_record) => { + if kinds.contains(&transaction_record.kind().to_string()) { + transactions.push(Arc::new(transaction_record)) + } + } + Err(err) => { + log_error!("Failed to deserialize transaction record from indexdb {:?}", err); + } + } + } + + located += 1; + + if let Ok(b) = cursor.continue_cursor() { + match b.await { + Ok(b) => { + if !b { + break; + } + } + Err(err) => { + log_info!("DEBUG IDB: Loading transaction error, cursor.continue_cursor() {:?}", err); + break; + } + } + } else { + break; + } + } else { + break; + } + } + } else if total > 0 { + if range.start > 0 { + let res = cursor + .advance(range.start as u32) + .map_err(|err| Error::Custom(format!("Unable to advance indexdb cursor {:?}", err)))? + .await; + let _res = res.map_err(|err| Error::Custom(format!("Unable to advance indexdb cursor future {:?}", err)))?; + // if !res { + // //return Err(Error::Custom(format!("Unable to advance indexdb cursor future {:?}", err))); + // } + } + + loop { + if transactions.len() < count { + match transaction_record_from_js_value(&cursor.value(), None) { + Ok(transaction_record) => transactions.push(Arc::new(transaction_record)), Err(err) => { - log_info!("DEBUG IDB: Loading transaction error, cursor.continue_cursor() {:?}", err); - break; + log_error!("Failed to deserialize transaction record from indexdb {:?}", err); } } + + if let Ok(b) = cursor.continue_cursor() { + match b.await { + Ok(b) => { + if !b { + break; + } + } + Err(err) => { + log_info!("DEBUG IDB: Loading transaction error, cursor.continue_cursor() {:?}", err); + break; + } + } + } else { + break; + } } else { break; } - } else { - break; } } } - let transactions = records - .iter() - .filter_map(|js_value| match transaction_record_from_js_value(js_value, None) { - Ok(transaction_record) => Some(Arc::new(transaction_record)), - Err(err) => { - log_error!("Failed to deserialize transaction record from indexdb {:?}", err); - None - } - }) - .collect::>(); Ok(TransactionRangeResult { transactions, total: total.into() }) }) @@ -601,11 +781,14 @@ fn transaction_record_to_js_value( let borsh_data_uint8_arr = Uint8Array::from(encryped_data_vec.as_slice()); let borsh_data_js_value = borsh_data_uint8_arr.into(); + let group_js_value = TransactionGroup::from(transaction_record.kind()).into(); + let kind_js_value = transaction_record.kind().to_string().into(); let obj = Object::new(); obj.set("id", &id_js_value)?; obj.set("timestamp", ×tamp_js_value)?; obj.set("data", &borsh_data_js_value)?; - + obj.set("group", &group_js_value)?; + obj.set("kind", &kind_js_value)?; let value = JsValue::from(obj); Ok(value) } diff --git a/wallet/core/src/storage/mod.rs b/wallet/core/src/storage/mod.rs index 2516bcd483..33537873c0 100644 --- a/wallet/core/src/storage/mod.rs +++ b/wallet/core/src/storage/mod.rs @@ -28,7 +28,7 @@ pub use keydata::{AssocPrvKeyDataIds, PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, pub use local::interface::make_filename; pub use metadata::AccountMetadata; pub use storable::Storable; -pub use transaction::{TransactionData, TransactionId, TransactionKind, TransactionRecord}; +pub use transaction::{TransactionData, TransactionGroup, TransactionId, TransactionKind, TransactionRecord}; #[cfg(test)] mod tests { diff --git a/wallet/core/src/storage/transaction/data.rs b/wallet/core/src/storage/transaction/data.rs index e976574fd1..27514e4d2d 100644 --- a/wallet/core/src/storage/transaction/data.rs +++ b/wallet/core/src/storage/transaction/data.rs @@ -72,6 +72,20 @@ pub enum TransactionData { #[serde(default)] utxo_entries: Vec, }, + #[serde(rename_all = "camelCase")] + Meta { + fees: u64, + #[serde(rename = "inputValue")] + aggregate_input_value: u64, + #[serde(rename = "outputValue")] + aggregate_output_value: u64, + transaction: Transaction, + payment_value: Option, + change_value: u64, + accepted_daa_score: Option, + #[serde(default)] + utxo_entries: Vec, + }, TransferIncoming { fees: u64, #[serde(rename = "inputValue")] @@ -132,6 +146,7 @@ impl TransactionData { TransactionData::Incoming { .. } => TransactionKind::Incoming, TransactionData::External { .. } => TransactionKind::External, TransactionData::Outgoing { .. } => TransactionKind::Outgoing, + TransactionData::Meta { .. } => TransactionKind::Meta, TransactionData::Batch { .. } => TransactionKind::Batch, TransactionData::TransferIncoming { .. } => TransactionKind::TransferIncoming, TransactionData::TransferOutgoing { .. } => TransactionKind::TransferOutgoing, @@ -146,6 +161,7 @@ impl TransactionData { TransactionData::Incoming { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), TransactionData::External { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), TransactionData::Outgoing { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::Meta { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), TransactionData::Batch { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), TransactionData::TransferIncoming { utxo_entries, .. } => { utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)) @@ -220,6 +236,25 @@ impl BorshSerialize for TransactionData { BorshSerialize::serialize(accepted_daa_score, writer)?; BorshSerialize::serialize(utxo_entries, writer)?; } + TransactionData::Meta { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + } => { + BorshSerialize::serialize(fees, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + BorshSerialize::serialize(aggregate_output_value, writer)?; + BorshSerialize::serialize(transaction, writer)?; + BorshSerialize::serialize(payment_value, writer)?; + BorshSerialize::serialize(change_value, writer)?; + BorshSerialize::serialize(accepted_daa_score, writer)?; + BorshSerialize::serialize(utxo_entries, writer)?; + } TransactionData::TransferIncoming { fees, aggregate_input_value, @@ -349,6 +384,26 @@ impl BorshDeserialize for TransactionData { utxo_entries, }) } + TransactionKind::Meta => { + let fees: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let transaction: Transaction = BorshDeserialize::deserialize_reader(reader)?; + let payment_value: Option = BorshDeserialize::deserialize_reader(reader)?; + let change_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize_reader(reader)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; + Ok(TransactionData::Meta { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + }) + } TransactionKind::TransferIncoming => { let fees: u64 = BorshDeserialize::deserialize_reader(reader)?; let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; diff --git a/wallet/core/src/storage/transaction/kind.rs b/wallet/core/src/storage/transaction/kind.rs index 22944a34cd..3638521678 100644 --- a/wallet/core/src/storage/transaction/kind.rs +++ b/wallet/core/src/storage/transaction/kind.rs @@ -23,11 +23,12 @@ export enum TransactionKind { External = "external", TransferIncoming = "transfer-incoming", TransferOutgoing = "transfer-outgoing", + Meta = "meta", } "#; // Do not change the order of the variants in this enum. -seal! { 0x93c6, { +seal! { 0x34bf, { #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum TransactionKind { @@ -65,6 +66,9 @@ seal! { 0x93c6, { /// Outgoing transfer transaction. A transfer between multiple /// accounts managed by the wallet runtime. TransferOutgoing, + /// Meta transaction. A transaction that is not a regular outgoing transaction + /// but used for krc20/krc721 meta transactions. + Meta, } } } @@ -76,6 +80,7 @@ impl TransactionKind { match self { TransactionKind::Incoming => "+", TransactionKind::Outgoing => "-", + TransactionKind::Meta => "-", TransactionKind::External => "-", TransactionKind::Batch => "", TransactionKind::Reorg => "-", @@ -93,6 +98,7 @@ impl std::fmt::Display for TransactionKind { let s = match self { TransactionKind::Incoming => "incoming", TransactionKind::Outgoing => "outgoing", + TransactionKind::Meta => "meta", TransactionKind::External => "external", TransactionKind::Batch => "batch", TransactionKind::Reorg => "reorg", @@ -113,6 +119,7 @@ impl TryFrom for TransactionKind { "incoming" => Ok(TransactionKind::Incoming), "outgoing" => Ok(TransactionKind::Outgoing), "external" => Ok(TransactionKind::External), + "meta" => Ok(TransactionKind::Meta), "batch" => Ok(TransactionKind::Batch), "reorg" => Ok(TransactionKind::Reorg), "stasis" => Ok(TransactionKind::Stasis), @@ -126,3 +133,57 @@ impl TryFrom for TransactionKind { } } } + +#[wasm_bindgen(typescript_custom_section)] +const TS_TRANSACTION_GROUP: &'static str = r#" +export enum TransactionGroup { + Native = "native", + Meta = "meta", +} +"#; + +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Eq, PartialEq)] +pub enum TransactionGroup { + Native, + Meta, +} + +impl std::fmt::Display for TransactionGroup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + TransactionGroup::Native => "native", + TransactionGroup::Meta => "meta", + }; + write!(f, "{s}") + } +} + +impl TryFrom for TransactionGroup { + type Error = Error; + fn try_from(js_value: JsValue) -> std::result::Result { + if let Some(s) = js_value.as_string() { + match s.as_str() { + "native" => Ok(TransactionGroup::Native), + "meta" => Ok(TransactionGroup::Meta), + _ => Err(Error::Custom(format!("InvalidTransactionGroup: {:?}", s))), + } + } else { + Err(Error::Custom(format!("InvalidTransactionGroup: {:?}", js_value))) + } + } +} +impl From for JsValue { + fn from(group: TransactionGroup) -> Self { + JsValue::from_str(group.to_string().as_str()) + } +} + +impl From for TransactionGroup { + fn from(kind: TransactionKind) -> Self { + if kind == TransactionKind::Meta { + TransactionGroup::Meta + } else { + TransactionGroup::Native + } + } +} diff --git a/wallet/core/src/storage/transaction/record.rs b/wallet/core/src/storage/transaction/record.rs index 05deb09b74..1f92f8e11f 100644 --- a/wallet/core/src/storage/transaction/record.rs +++ b/wallet/core/src/storage/transaction/record.rs @@ -6,6 +6,7 @@ use super::*; use crate::imports::*; use crate::storage::{Binding, BindingT}; use crate::tx::PendingTransactionInner; +use crate::utils::to_js_value_with_u64_as_bigint; use workflow_core::time::{unixtime_as_millis_u64, unixtime_to_locale_string}; use workflow_wasm::utils::try_get_js_value_prop; @@ -23,7 +24,7 @@ export interface IUtxoRecord { address?: Address; index: number; amount: bigint; - scriptPublicKey: HexString; + scriptPubKey: HexString; isCoinbase: boolean; } @@ -62,6 +63,11 @@ export enum TransactionDataType { * @see {@link ITransactionDataOutgoing} */ Outgoing = "outgoing", + /** + * Transaction is a meta transaction. + * @see {@link ITransactionDataMeta} + */ + Meta = "meta", /** * Transaction is a batch transaction (compounding UTXOs to an internal change address). * @see {@link ITransactionDataBatch} @@ -156,6 +162,21 @@ export interface ITransactionDataOutgoing { utxoEntries: IUtxoRecord[]; } +/** + * Meta transaction data. + * @category Wallet SDK + */ +export interface ITransactionDataMeta { + fees: bigint; + aggregateInputValue: bigint; + aggregateOutputValue: bigint; + transaction: ITransaction; + paymentValue: bigint; + changeValue: bigint; + acceptedDaaScore?: bigint; + utxoEntries: IUtxoRecord[]; +} + /** * Incoming transfer transaction data. * Transfer occurs when a transaction is issued between @@ -464,6 +485,7 @@ impl TransactionRecord { | TransactionData::Incoming { aggregate_input_value, .. } | TransactionData::External { aggregate_input_value, .. } | TransactionData::Outgoing { aggregate_input_value, .. } + | TransactionData::Meta { aggregate_input_value, .. } | TransactionData::Batch { aggregate_input_value, .. } | TransactionData::TransferIncoming { aggregate_input_value, .. } | TransactionData::TransferOutgoing { aggregate_input_value, .. } @@ -546,11 +568,22 @@ impl TransactionRecord { note: None, } } - pub fn new_outgoing( utxo_context: &UtxoContext, outgoing_tx: &OutgoingTransaction, accepted_daa_score: Option, + ) -> Result { + Self::new_outgoing_impl(utxo_context, outgoing_tx, accepted_daa_score, false) + } + pub fn new_meta(utxo_context: &UtxoContext, outgoing_tx: &OutgoingTransaction, accepted_daa_score: Option) -> Result { + Self::new_outgoing_impl(utxo_context, outgoing_tx, accepted_daa_score, true) + } + + fn new_outgoing_impl( + utxo_context: &UtxoContext, + outgoing_tx: &OutgoingTransaction, + accepted_daa_score: Option, + is_meta_tx: bool, ) -> Result { let binding = Binding::from(utxo_context.binding()); let block_daa_score = @@ -573,15 +606,28 @@ impl TransactionRecord { let transaction = signable_tx.lock().unwrap().tx.clone(); let id = transaction.id(); - let transaction_data = TransactionData::Outgoing { - fees: *fees, - aggregate_input_value: *aggregate_input_value, - aggregate_output_value: *aggregate_output_value, - transaction, - payment_value: *payment_value, - change_value: *change_output_value, - accepted_daa_score, - utxo_entries, + let transaction_data = if is_meta_tx { + TransactionData::Meta { + fees: *fees, + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + } + } else { + TransactionData::Outgoing { + fees: *fees, + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + } }; Ok(TransactionRecord { @@ -815,7 +861,8 @@ impl TransactionRecord { #[wasm_bindgen(getter, js_name = "data")] pub fn data_as_js_value(&self) -> TransactionDataT { - try_get_js_value_prop(&serde_wasm_bindgen::to_value(&self.transaction_data).unwrap(), "data").unwrap().unchecked_into() + let value = to_js_value_with_u64_as_bigint(&self.transaction_data).unwrap(); + try_get_js_value_prop(&value, "data").unwrap().unchecked_into() } #[wasm_bindgen(getter, js_name = "type")] diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index 9a3bbdd489..cefd501655 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -223,7 +223,7 @@ impl PendingTransaction { } /// Submit the transaction on the supplied rpc - pub async fn try_submit(&self, rpc: &Arc) -> Result { + pub async fn try_submit(&self, rpc: &Arc, is_meta_tx: bool) -> Result { // sanity check to prevent multiple invocations (for API use) self.inner.is_submitted.load(Ordering::SeqCst).then(|| { panic!("PendingTransaction::try_submit() called multiple times"); @@ -238,7 +238,7 @@ impl PendingTransaction { let _lock = utxo_context.processor().notification_lock().await; // register pending UTXOs with UtxoProcessor - utxo_context.register_outgoing_transaction(self).await?; + utxo_context.register_outgoing_transaction(self, is_meta_tx).await?; // try to submit transaction match rpc.submit_transaction(rpc_transaction, false).await { diff --git a/wallet/core/src/utils.rs b/wallet/core/src/utils.rs index c732e3f68d..3664e9504c 100644 --- a/wallet/core/src/utils.rs +++ b/wallet/core/src/utils.rs @@ -6,8 +6,189 @@ use crate::result::Result; use kaspa_addresses::Address; use kaspa_consensus_core::constants::*; use kaspa_consensus_core::network::NetworkType; +use workflow_core::task::sleep; +use workflow_core::time::Duration; +//use kaspa_consensus_core::subnets::SubnetworkId; +use crate::error::Error; use separator::{separated_float, separated_int, separated_uint_with_output, Separatable}; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; +use wasm_bindgen::JsValue; +use workflow_http::get_json; +use workflow_log::log_warn; use workflow_log::style; +// use crate::utxo::context::UtxoContext; +// use crate::storage::transaction::record::TransactionRecord; +// use crate::storage::transaction::data::TransactionData; +// use crate::utxo::UtxoEntryReference; +// use crate::storage::transaction::UtxoRecord; +// use std::str::FromStr; +// use kaspa_hashes::Hash; + +// Add Transaction struct +#[derive(Debug, Serialize, Deserialize)] +pub struct Transaction { + pub subnetwork_id: String, + pub transaction_id: String, + pub hash: String, + pub mass: String, + pub payload: Option, + pub block_hash: Vec, + pub block_time: u64, + pub is_accepted: bool, + pub accepting_block_hash: String, + pub accepting_block_blue_score: u64, + pub inputs: Vec, + pub outputs: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TransactionInput { + pub transaction_id: String, + pub index: u32, + pub previous_outpoint_hash: String, + pub previous_outpoint_index: String, + pub previous_outpoint_resolved: PreviousOutpointResolved, + pub previous_outpoint_address: String, + pub previous_outpoint_amount: u64, + pub signature_script: String, + pub sig_op_count: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PreviousOutpointResolved { + pub transaction_id: String, + pub index: u32, + pub amount: u64, + pub script_public_key: String, + pub script_public_key_address: String, + pub script_public_key_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TransactionOutput { + pub transaction_id: String, + pub index: u32, + pub amount: u64, + pub script_public_key: String, + pub script_public_key_address: String, + pub script_public_key_type: String, +} + +// impl TryFrom for kaspa_consensus_core::tx::Transaction { +// type Error = Error; +// fn try_from(tx: Transaction) -> Result { +// Ok(Self::new(0, tx.inputs.try_into()?, +// tx.outputs.try_into()?, 0, SubnetworkId::from_str(&tx.subnetwork_id)?, 0, tx.payload.as_slice().to_vec())) +// } +// } + +// impl TryFrom for kaspa_consensus_core::tx::TransactionInput { +// type Error = Error; +// fn try_from(input: TransactionInput) -> Result { +// let sequence = 0;//input.sequence.parse::()?; +// let previous_outpoint = kaspa_consensus_core::tx::TransactionOutpoint::new(Hash::from_str(&input.previous_outpoint_hash)?, input.previous_outpoint_index.parse::()?); +// Ok(Self::new( +// previous_outpoint, +// input.signature_script.as_slice().to_vec().into(), +// sequence, +// input.sig_op_count.parse::()?)) +// } +// } + +// impl Transaction { + +// pub fn meta_transaction_record(&self, utxo_context: &UtxoContext, utxos: &Vec) -> Result { + +// let block_daa_score = utxos[0].utxo.block_daa_score; +// let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); +// let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); +// let aggregate_output_value = self.outputs.iter().map(|output| output.amount).sum::(); + +// let transaction_data = TransactionData::Meta{ +// fees: 0, +// aggregate_input_value, +// aggregate_output_value, +// transaction: self.into(), +// payment_value: None, +// change_value: 0, +// accepted_daa_score: Some(block_daa_score), +// utxo_entries, +// }; +// let transaction_record = TransactionRecord{ +// id: Hash::from_str(&self.transaction_id)?, +// unixtime_msec: Some(self.block_time), +// value: aggregate_input_value, +// binding: utxo_context.binding().into(), +// transaction_data, +// block_daa_score, +// network_id: utxo_context.processor().network_id()?, +// metadata: None, +// note: None, +// }; + +// Ok(transaction_record) +// } +// } + +//KASPLEX +pub const KASPLEX_HEADER_LC: &[u8] = b"kasplex"; // &[107, 97, 115, 112, 108, 101, 120] +pub const KASPLEX_HEADER_LC_HEX: &str = "6b6173706c6578"; +pub const KASPLEX_HEADER_UC: &[u8] = b"KASPLEX"; // &[75, 65, 83, 80, 76, 69, 88] +pub const KASPLEX_HEADER_UC_HEX: &str = "4b4153504c4558"; + +//KSPR +pub const KSPR_HEADER_LC: &[u8] = b"kspr"; // &[107, 115, 112, 114] +pub const KSPR_HEADER_LC_HEX: &str = "6b737072"; +pub const KSPR_HEADER_UC: &[u8] = b"KSPR"; // &[75, 83, 80, 82] +pub const KSPR_HEADER_UC_HEX: &str = "4b535052"; + +//KRC20 +pub const KRC20_HEADER_LC: &[u8] = b"krc-20"; // &[107, 114, 99, 45, 50, 48] +pub const KRC20_HEADER_LC_HEX: &str = "6b72632d3230"; +pub const KRC20_HEADER_UC: &[u8] = b"KRC-20"; // &[75, 82, 67, 45, 50, 48] +pub const KRC20_HEADER_UC_HEX: &str = "4b52432d3230"; + +//KRC721 +pub const KRC721_HEADER_LC: &[u8] = b"krc-721"; // &[107, 114, 99, 45, 55, 50, 49] +pub const KRC721_HEADER_LC_HEX: &str = "6b72632d373231"; +pub const KRC721_HEADER_UC: &[u8] = b"KRC-721"; // &[75, 82, 67, 45, 55, 50, 49] +pub const KRC721_HEADER_UC_HEX: &str = "4b52432d373231"; + +pub fn detect_krc20_or_krc721(signature: &str) -> bool { + signature.contains(KRC20_HEADER_LC_HEX) || signature.contains(KRC721_HEADER_LC_HEX) +} + +pub fn detect_kspr_or_kasplex(signature: &str) -> bool { + signature.contains(KSPR_HEADER_LC_HEX) || signature.contains(KASPLEX_HEADER_LC_HEX) +} + +pub fn detect_meta_tokens(signature: &str) -> bool { + let signature = signature.to_lowercase(); + detect_kspr_or_kasplex(&signature) && detect_krc20_or_krc721(&signature) +} + +pub async fn get_transaction_by_id(txid: &str) -> Result { + let url = format!("https://api.kaspa.org/transactions/{}", txid); + + let res = get_json::(&url).await.map_err(|e| Error::custom(e.to_string())); + log_warn!("### get_transaction_by_id1: {txid} {:?}", res); + match res { + Ok(tx) => Ok(tx), + Err(_) => { + let res = async { + sleep(Duration::from_secs(1)).await; + get_json::(&url).await.map_err(|e| Error::custom(e.to_string())) + } + .await; + if res.is_err() { + log_warn!("### get_transaction_by_id2: {txid} {:?}", res); + } + res + } + } +} pub fn try_kaspa_str_to_sompi>(s: S) -> Result> { let s: String = s.into(); @@ -108,3 +289,31 @@ fn str_to_sompi(amount: &str) -> Result { }; Ok(integer + decimal) } + +// Helper function to recursively convert `u64` to bigint in `serde_json::Value`. +pub fn convert_u64_to_bigint(value: Value) -> Result { + match value { + Value::Number(num) if num.is_u64() => Ok(js_sys::BigInt::from(num.as_u64().unwrap()).into()), + Value::Array(arr) => { + let mut values = Vec::new(); + for v in arr { + values.push(convert_u64_to_bigint(v)?); + } + Ok(js_sys::Array::from_iter(values).into()) + } + Value::Object(map) => { + let obj = js_sys::Object::new(); + for (k, v) in map { + js_sys::Reflect::set(&obj, &JsValue::from(k), &convert_u64_to_bigint(v)?)?; + } + Ok(obj.into()) + } + _ => Ok(serde_wasm_bindgen::to_value(&value)?), + } +} + +// Main function to serialize the enum to `JsValue` +pub fn to_js_value_with_u64_as_bigint(value: &T) -> Result { + let json_value = serde_json::to_value(value)?; + convert_u64_to_bigint(json_value) +} diff --git a/wallet/core/src/utxo/context.rs b/wallet/core/src/utxo/context.rs index 39575a64f6..c343762cf9 100644 --- a/wallet/core/src/utxo/context.rs +++ b/wallet/core/src/utxo/context.rs @@ -10,6 +10,7 @@ use crate::imports::*; use crate::result::Result; use crate::storage::TransactionRecord; use crate::tx::PendingTransaction; +use crate::utils::{detect_meta_tokens, get_transaction_by_id}; use crate::utxo::{ Maturity, NetworkParams, OutgoingTransaction, PendingUtxoEntryReference, UtxoContextBinding, UtxoEntryId, UtxoEntryReference, UtxoEntryReferenceExtension, UtxoProcessor, @@ -92,6 +93,8 @@ pub struct Context { /// Confirmation occurs when the transaction UTXOs are /// removed from the context by the UTXO change notification. pub(crate) outgoing: AHashMap, + /// Outgoing transactions that have used for krc20, krc721 transactions. + pub(crate) meta: AHashMap, /// Total balance of all UTXOs in this context (mature, pending) balance: Option, /// Addresses monitored by this UTXO context @@ -106,6 +109,7 @@ impl Default for Context { stasis: AHashMap::default(), map: AHashMap::default(), outgoing: AHashMap::default(), + meta: AHashMap::default(), balance: None, addresses: Arc::new(DashSet::new()), } @@ -123,6 +127,7 @@ impl Context { self.stasis.clear(); self.pending.clear(); self.outgoing.clear(); + self.meta.clear(); self.addresses.clear(); self.balance = None; } @@ -243,7 +248,7 @@ impl UtxoContext { /// Process pending transaction. Remove mature UTXO entries and add them to the consumed set. /// Produces a notification on the even multiplexer. - pub(crate) async fn register_outgoing_transaction(&self, pending_tx: &PendingTransaction) -> Result<()> { + pub(crate) async fn register_outgoing_transaction(&self, pending_tx: &PendingTransaction, is_meta_tx: bool) -> Result<()> { { let current_daa_score = self.processor().current_daa_score().ok_or(Error::MissingDaaScore("register_outgoing_transaction()"))?; @@ -254,21 +259,32 @@ impl UtxoContext { let outgoing_transaction = OutgoingTransaction::new(current_daa_score, self.clone(), pending_tx.clone()); self.processor().register_outgoing_transaction(outgoing_transaction.clone()); - context.outgoing.insert(outgoing_transaction.id(), outgoing_transaction); + if is_meta_tx { + context.meta.insert(outgoing_transaction.id(), outgoing_transaction); + } else { + context.outgoing.insert(outgoing_transaction.id(), outgoing_transaction); + } } Ok(()) } pub(crate) async fn notify_outgoing_transaction(&self, pending_tx: &PendingTransaction) -> Result<()> { - let outgoing_tx = self.processor().outgoing().get(&pending_tx.id()).expect("outgoing transaction for notification"); + let record = { self.context().meta.get(&pending_tx.id()).cloned() }; - if pending_tx.is_batch() { - let record = TransactionRecord::new_batch(self, &outgoing_tx, None)?; + if let Some(outgoing_tx) = record { + let record = TransactionRecord::new_meta(self, &outgoing_tx, None)?; self.processor().notify(Events::Pending { record }).await?; } else { - let record = TransactionRecord::new_outgoing(self, &outgoing_tx, None)?; - self.processor().notify(Events::Pending { record }).await?; + let outgoing_tx = self.processor().outgoing().get(&pending_tx.id()).expect("outgoing transaction for notification"); + + if pending_tx.is_batch() { + let record = TransactionRecord::new_batch(self, &outgoing_tx, None)?; + self.processor().notify(Events::Pending { record }).await?; + } else { + let record = TransactionRecord::new_outgoing(self, &outgoing_tx, None)?; + self.processor().notify(Events::Pending { record }).await?; + } } self.update_balance().await?; Ok(()) @@ -547,7 +563,6 @@ impl UtxoContext { if let Some(outgoing_transaction) = outgoing_transaction { accepted_outgoing_transactions.insert((*outgoing_transaction).clone()); - if outgoing_transaction.is_batch() { let record = TransactionRecord::new_batch(self, &outgoing_transaction, Some(current_daa_score))?; self.processor().notify(Events::Maturity { record }).await?; @@ -561,8 +576,30 @@ impl UtxoContext { } } else if !is_coinbase_stasis { // do not notify if coinbase transaction is in stasis - let record = TransactionRecord::new_incoming(self, txid, &utxos); - self.processor().notify(Events::Pending { record }).await?; + log_warn!("### incoming transaction: {:?}", txid); + let record = if let Ok(tx) = get_transaction_by_id(&txid.to_string()).await { + log_warn!("### incoming transaction tx: {:?}", tx); + if tx.inputs.is_not_empty() && detect_meta_tokens(&tx.inputs[0].signature_script) { + log_warn!("Skipping Meta transaction: {:?}", tx); + // TODO: Implement meta transaction record creation + //match tx.meta_transaction_record(self, &utxos) { + // Ok(record) => record, + // Err(e) => { + // log_error!("Error: unable to create meta transaction record from tx: {:?}, error: {:?}", tx, e); + // return Err(e); + // } + // } + None + } else { + log_warn!("### incoming transaction tx.inputs: {:?}", tx.inputs); + Some(TransactionRecord::new_incoming(self, txid, &utxos)) + } + } else { + Some(TransactionRecord::new_incoming(self, txid, &utxos)) + }; + if let Some(record) = record { + self.processor().notify(Events::Pending { record }).await?; + } } } @@ -589,7 +626,10 @@ impl UtxoContext { } for accepted_outgoing_transaction in accepted_outgoing_transactions.into_iter() { - if accepted_outgoing_transaction.is_batch() { + if self.context().meta.contains_key(&accepted_outgoing_transaction.id()) { + let record = TransactionRecord::new_meta(self, &accepted_outgoing_transaction, Some(current_daa_score))?; + self.processor().notify(Events::Maturity { record }).await?; + } else if accepted_outgoing_transaction.is_batch() { let record = TransactionRecord::new_batch(self, &accepted_outgoing_transaction, Some(current_daa_score))?; self.processor().notify(Events::Maturity { record }).await?; } else if accepted_outgoing_transaction.destination_context().is_some() { diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs index f7a988097d..7fc59d19fb 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -479,7 +479,7 @@ impl WalletApi for super::Wallet { ) .await?; - let transaction_ids = account.pskb_broadcast(&bundle).await?; + let transaction_ids = account.pskb_broadcast(&bundle, true).await?; Ok(AccountsCommitRevealManualResponse { transaction_ids }) } @@ -529,7 +529,7 @@ impl WalletApi for super::Wallet { ) .await?; - let transaction_ids = account.pskb_broadcast(&bundle).await?; + let transaction_ids = account.pskb_broadcast(&bundle, true).await?; Ok(AccountsCommitRevealResponse { transaction_ids }) } @@ -560,7 +560,7 @@ impl WalletApi for super::Wallet { } async fn transactions_data_get_call(self: Arc, request: TransactionsDataGetRequest) -> Result { - let TransactionsDataGetRequest { account_id, network_id, filter, start, end } = request; + let TransactionsDataGetRequest { account_id, network_id, kind_filter, group_filter, start, end } = request; if start > end { return Err(Error::InvalidRange(start, end)); @@ -569,7 +569,7 @@ impl WalletApi for super::Wallet { let binding = Binding::Account(account_id); let store = self.store().as_transaction_record_store()?; let TransactionRangeResult { transactions, total } = - store.load_range(&binding, &network_id, filter, start as usize..end as usize).await?; + store.load_range(&binding, &network_id, kind_filter, group_filter, start as usize..end as usize).await?; Ok(TransactionsDataGetResponse { transactions, total, account_id, start }) } diff --git a/wallet/core/src/wasm/api/message.rs b/wallet/core/src/wasm/api/message.rs index b94a245463..21a2e46bac 100644 --- a/wallet/core/src/wasm/api/message.rs +++ b/wallet/core/src/wasm/api/message.rs @@ -1651,7 +1651,8 @@ declare! { export interface ITransactionsDataGetRequest { accountId : HexString; networkId : NetworkId | string; - filter? : TransactionKind[]; + kindFilter? : TransactionKind[]; + groupFilter? : TransactionGroup[]; start : bigint; end : bigint; } @@ -1661,8 +1662,11 @@ declare! { try_from! ( args: ITransactionsDataGetRequest, TransactionsDataGetRequest, { let account_id = args.get_account_id("accountId")?; let network_id = args.get_network_id("networkId")?; - let filter = args.get_vec("filter").ok().map(|filter| { - filter.into_iter().map(TransactionKind::try_from).collect::>>() + let kind_filter = args.get_vec("kindFilter").ok().map(|kind_filter| { + kind_filter.into_iter().map(TransactionKind::try_from).collect::>>() + }).transpose()?; + let group_filter = args.get_vec("groupFilter").ok().map(|group_filter| { + group_filter.into_iter().map(TransactionGroup::try_from).collect::>>() }).transpose()?; let start = args.get_u64("start")?; let end = args.get_u64("end")?; @@ -1670,7 +1674,8 @@ try_from! ( args: ITransactionsDataGetRequest, TransactionsDataGetRequest, { let request = TransactionsDataGetRequest { account_id, network_id, - filter, + kind_filter, + group_filter, start, end, }; diff --git a/wallet/core/src/wasm/tx/generator/pending.rs b/wallet/core/src/wasm/tx/generator/pending.rs index 58c36375d6..f662cc2372 100644 --- a/wallet/core/src/wasm/tx/generator/pending.rs +++ b/wallet/core/src/wasm/tx/generator/pending.rs @@ -151,14 +151,14 @@ impl PendingTransaction { /// # Important /// /// Make sure to consume the returned `txid` value. Always invoke this method - /// as follows `let txid = await pendingTransaction.submit(rpc);`. If you do not + /// as follows `let txid = await pendingTransaction.submit(rpc, is_meta_tx);`. If you do not /// consume the returned value and the rpc object is temporary, the GC will /// collect the `rpc` object passed to submit() potentially causing a panic. /// /// @see {@link RpcClient.submitTransaction} - pub async fn submit(&self, wasm_rpc_client: &RpcClient) -> Result { + pub async fn submit(&self, wasm_rpc_client: &RpcClient, is_meta_tx: bool) -> Result { let rpc: Arc = wasm_rpc_client.client().clone(); - let txid = self.inner.try_submit(&rpc).await?; + let txid = self.inner.try_submit(&rpc, is_meta_tx).await?; Ok(txid.to_string()) }