diff --git a/Cargo.lock b/Cargo.lock index e6c8605dd5f08e407a3675a5390fe23d650f2e25..057d05b2854a8226f99a5ee248795bbd9d1e9a59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,6 +1172,7 @@ dependencies = [ "anyhow", "dubp", "duniter-dbs", + "maplit", "resiter", "smallvec", ] @@ -1183,6 +1184,7 @@ dependencies = [ "anyhow", "dubp", "duniter-dbs", + "maplit", "resiter", "smallvec", ] diff --git a/rust-libs/duniter-dbs/src/databases/gva_v1.rs b/rust-libs/duniter-dbs/src/databases/gva_v1.rs index bc9ab413f23301f7131d18288e444ee6e62989bd..4990d0053774ab0d5a49af0d8b0e9b875029b0b8 100644 --- a/rust-libs/duniter-dbs/src/databases/gva_v1.rs +++ b/rust-libs/duniter-dbs/src/databases/gva_v1.rs @@ -21,8 +21,8 @@ db_schema!( ["blocks_with_ud", BlocksWithUd, U32BE, ()], ["blockchain_time", BlockchainTime, U32BE, u64], ["txs", Txs, HashKeyV2, TxDbV2], - ["txs_by_issuer", TxsByIssuer, PubKeyKeyV2, Vec<Hash>], - ["txs_by_recipient", TxsByRecipient, PubKeyKeyV2, Vec<Hash>], + ["txs_by_issuer", TxsByIssuer, WalletHashWithBnV1Db, BTreeSet<Hash>], + ["txs_by_recipient", TxsByRecipient, WalletHashWithBnV1Db, BTreeSet<Hash>], [ "scripts_by_pubkey", ScriptsByPubkey, diff --git a/rust-libs/duniter-dbs/src/keys.rs b/rust-libs/duniter-dbs/src/keys.rs index b5d2db16bd1451130a4bca39686bba24cfd933a1..6f6e1f1d3aa1a404072429e1ca82eaf3cb162ecb 100644 --- a/rust-libs/duniter-dbs/src/keys.rs +++ b/rust-libs/duniter-dbs/src/keys.rs @@ -26,3 +26,4 @@ pub mod ud_id; pub mod uid; pub mod utxo_id; pub mod wallet_conditions; +pub mod wallet_hash_with_bn; diff --git a/rust-libs/duniter-dbs/src/keys/wallet_hash_with_bn.rs b/rust-libs/duniter-dbs/src/keys/wallet_hash_with_bn.rs new file mode 100644 index 0000000000000000000000000000000000000000..25b3d32d9398ab330799a3b9cd22ea7a740b5367 --- /dev/null +++ b/rust-libs/duniter-dbs/src/keys/wallet_hash_with_bn.rs @@ -0,0 +1,113 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +use std::fmt::Display; +use uninit::prelude::*; + +#[derive( + Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, zerocopy::AsBytes, zerocopy::FromBytes, +)] +#[repr(transparent)] +pub struct WalletHashWithBnV1Db([u8; 36]); // wallet_hash ++ block_number + +impl WalletHashWithBnV1Db { + pub fn new(hash: Hash, block_number: BlockNumber) -> Self { + let mut buffer = uninit_array![u8; 36]; + let (hash_buffer, bn_buffer) = buffer.as_out().split_at_out(32); + + hash_buffer.copy_from_slice(hash.as_ref()); + bn_buffer.copy_from_slice(&block_number.0.to_be_bytes()[..]); + + Self(unsafe { std::mem::transmute(buffer) }) + } + pub fn get_wallet_hash(&self) -> Hash { + let mut buffer = uninit_array![u8; 32]; + + buffer.as_out().copy_from_slice(&self.0[..32]); + let bytes: [u8; 32] = unsafe { std::mem::transmute(buffer) }; + + Hash(bytes) + } + pub fn get_block_number(&self) -> u32 { + let mut buffer = uninit_array![u8; 4]; + + buffer.as_out().copy_from_slice(&self.0[32..]); + + u32::from_be_bytes(unsafe { std::mem::transmute(buffer) }) + } +} + +impl Default for WalletHashWithBnV1Db { + fn default() -> Self { + WalletHashWithBnV1Db([0u8; 36]) + } +} + +impl Display for WalletHashWithBnV1Db { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.get_wallet_hash(), self.get_block_number()) + } +} + +impl KeyAsBytes for WalletHashWithBnV1Db { + fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T { + f(self.0.as_ref()) + } +} + +impl kv_typed::prelude::FromBytes for WalletHashWithBnV1Db { + type Err = StringErr; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + let layout = zerocopy::LayoutVerified::<_, WalletHashWithBnV1Db>::new(bytes) + .ok_or_else(|| StringErr("corrupted db".to_owned()))?; + Ok(*layout) + } +} + +impl KeyZc for WalletHashWithBnV1Db { + type Ref = Self; +} + +impl ToDumpString for WalletHashWithBnV1Db { + fn to_dump_string(&self) -> String { + todo!() + } +} + +#[cfg(feature = "explorer")] +impl ExplorableKey for WalletHashWithBnV1Db { + fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> { + let mut source = source.split(':'); + let hash_str = source + .next() + .ok_or_else(|| StringErr("missing hash".to_owned()))?; + let bn_str = source + .next() + .ok_or_else(|| StringErr("missing block number".to_owned()))?; + + let hash = Hash::from_hex(hash_str).map_err(|e| StringErr(e.to_string()))?; + let block_number = bn_str + .parse() + .map_err(|e: std::num::ParseIntError| StringErr(e.to_string()))?; + + Ok(WalletHashWithBnV1Db::new(hash, block_number)) + } + fn to_explorer_string(&self) -> KvResult<String> { + Ok(self.to_string()) + } +} diff --git a/rust-libs/duniter-dbs/src/lib.rs b/rust-libs/duniter-dbs/src/lib.rs index 480936e7a82d02fb0129d4b5ccb509f4fcc8dfda..d7f69fd1d48e9dc1b099cdb7e81248800df2ac1b 100644 --- a/rust-libs/duniter-dbs/src/lib.rs +++ b/rust-libs/duniter-dbs/src/lib.rs @@ -66,6 +66,7 @@ pub use keys::ud_id::UdIdV2; pub use keys::uid::UidKeyV1; pub use keys::utxo_id::GvaUtxoIdDbV1; pub use keys::wallet_conditions::{WalletConditionsV1, WalletConditionsV2}; +pub use keys::wallet_hash_with_bn::WalletHashWithBnV1Db; pub use values::block_db::{BlockDbEnum, BlockDbV1, TransactionInBlockDbV1}; pub use values::block_head_db::BlockHeadDbV1; pub use values::block_meta::BlockMetaV2; diff --git a/rust-libs/modules/gva/db-writer/Cargo.toml b/rust-libs/modules/gva/db-writer/Cargo.toml index 801f2f15ba2534460de32e53870635094cdc4735..9c2e2ec0e114656626ce8d6d01ec5d2bfe675189 100644 --- a/rust-libs/modules/gva/db-writer/Cargo.toml +++ b/rust-libs/modules/gva/db-writer/Cargo.toml @@ -18,4 +18,6 @@ dubp = { version = "0.34.0" } resiter = "0.4.0" [dev-dependencies] +duniter-dbs = { path = "../../../duniter-dbs", features = ["mem"] } +maplit = "1.0.2" smallvec = { version = "1.4.0", features = ["serde", "write"] } diff --git a/rust-libs/modules/gva/db-writer/src/lib.rs b/rust-libs/modules/gva/db-writer/src/lib.rs index 2c6a309d8c035321f25827d15fbb84e2f2836e4c..36df7c5a8ad531ef27f7c51f478e1d5d3feca01a 100644 --- a/rust-libs/modules/gva/db-writer/src/lib.rs +++ b/rust-libs/modules/gva/db-writer/src/lib.rs @@ -40,7 +40,7 @@ use duniter_dbs::{ HashKeyV2, PubKeyKeyV2, SourceAmountValV2, TxDbV2, WalletConditionsV2, }; use resiter::filter::Filter; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; pub struct UtxoV10<'s> { pub id: UtxoIdV10, @@ -188,6 +188,8 @@ fn apply_block_txs<B: Backend>( txs: &[TransactionDocumentV10], ) -> KvResult<()> { let mut scripts_index = HashMap::new(); + let mut txs_by_issuer_mem = HashMap::new(); + let mut txs_by_recipient_mem = HashMap::new(); for tx in txs { let tx_hash = tx.get_hash(); // Write tx and update sources @@ -198,8 +200,23 @@ fn apply_block_txs<B: Backend>( &mut scripts_index, tx_hash, tx, + &mut txs_by_issuer_mem, + &mut txs_by_recipient_mem, )?; } + ( + gva_db.txs_by_issuer_write(), + gva_db.txs_by_recipient_write(), + ) + .write(|(mut txs_by_issuer, mut txs_by_recipient)| { + for (k, v) in txs_by_issuer_mem { + txs_by_issuer.upsert(k, v); + } + for (k, v) in txs_by_recipient_mem { + txs_by_recipient.upsert(k, v); + } + Ok(()) + })?; Ok(()) } diff --git a/rust-libs/modules/gva/db-writer/src/tx.rs b/rust-libs/modules/gva/db-writer/src/tx.rs index 2a5e1836b43eeb29bc569be27eb0d72fce322f17..ba0b62eb61068a862ef2d354723440913215526f 100644 --- a/rust-libs/modules/gva/db-writer/src/tx.rs +++ b/rust-libs/modules/gva/db-writer/src/tx.rs @@ -14,7 +14,7 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. use crate::*; -use duniter_dbs::databases::gva_v1::BalancesEvent; +use duniter_dbs::{databases::gva_v1::BalancesEvent, WalletHashWithBnV1Db}; pub(crate) type ScriptsHash = HashMap<WalletScriptV10, Hash>; @@ -28,6 +28,7 @@ fn get_script_hash(script: &WalletScriptV10, scripts_hash: &mut ScriptsHash) -> } } +#[allow(clippy::too_many_arguments)] pub(crate) fn apply_tx<B: Backend>( current_blockstamp: Blockstamp, current_time: i64, @@ -35,43 +36,22 @@ pub(crate) fn apply_tx<B: Backend>( scripts_hash: &mut ScriptsHash, tx_hash: Hash, tx: &TransactionDocumentV10, + txs_by_issuer_mem: &mut HashMap<WalletHashWithBnV1Db, BTreeSet<Hash>>, + txs_by_recipient_mem: &mut HashMap<WalletHashWithBnV1Db, BTreeSet<Hash>>, ) -> KvResult<()> { ( gva_db.scripts_by_pubkey_write(), - gva_db.txs_by_issuer_write(), - gva_db.txs_by_recipient_write(), gva_db.txs_write(), gva_db.gva_utxos_write(), gva_db.balances_write(), ) .write( - |( - mut scripts_by_pubkey, - mut txs_by_issuer, - mut txs_by_recipient, - mut txs, - mut gva_utxos, - mut balances, - )| { - // Insert on col `txs_by_issuer` - for pubkey in tx.issuers() { - let mut hashs = txs_by_issuer.get(&PubKeyKeyV2(pubkey))?.unwrap_or_default(); - hashs.push(tx_hash); - txs_by_issuer.upsert(PubKeyKeyV2(pubkey), hashs); - } - // Insert on col `txs_by_recipient` - for pubkey in tx.recipients_keys() { - let mut hashs = txs_by_recipient - .get(&PubKeyKeyV2(pubkey))? - .unwrap_or_default(); - hashs.push(tx_hash); - txs_by_recipient.upsert(PubKeyKeyV2(pubkey), hashs); - } - - // Remove consumed UTXOs + |(mut scripts_by_pubkey, mut txs, mut gva_utxos, mut balances)| { + let mut issuers_scripts_hashs = BTreeSet::new(); for input in tx.get_inputs() { - let account_script = match input.id { + let (account_script_hash, account_script) = match input.id { SourceIdV10::Utxo(utxo_id) => { + // Get issuer script & written block let db_tx_origin = gva_db .txs() .get(&HashKeyV2::from_ref(&utxo_id.tx_hash))? @@ -85,26 +65,42 @@ pub(crate) fn apply_tx<B: Backend>( .conditions .script .clone(); + let utxo_script_hash = get_script_hash(&utxo_script, scripts_hash); + + // Remove consumed UTXOs super::utxos::remove_utxo_v10::<B>( &mut scripts_by_pubkey, &mut gva_utxos, utxo_id, &utxo_script, - get_script_hash(&utxo_script, scripts_hash), + utxo_script_hash, db_tx_origin.written_block.number.0, )?; - utxo_script + + // Return utxo_script with hash + (utxo_script_hash, utxo_script) } SourceIdV10::Ud(UdSourceIdV10 { issuer, .. }) => { - WalletScriptV10::single_sig(issuer) + let script = WalletScriptV10::single_sig(issuer); + (Hash::compute(script.to_string().as_bytes()), script) } }; + issuers_scripts_hashs.insert(account_script_hash); + // Insert on col `txs_by_issuer` + txs_by_issuer_mem + .entry(WalletHashWithBnV1Db::new( + account_script_hash, + current_blockstamp.number, + )) + .or_default() + .insert(tx_hash); // Decrease account balance decrease_account_balance::<B>(account_script, &mut balances, input.amount)?; } - // Insert created UTXOs for (output_index, output) in tx.get_outputs().iter().enumerate() { + let utxo_script_hash = get_script_hash(&output.conditions.script, scripts_hash); + // Insert created UTXOs super::utxos::write_utxo_v10::<B>( &mut scripts_by_pubkey, &mut gva_utxos, @@ -117,9 +113,20 @@ pub(crate) fn apply_tx<B: Backend>( script: &output.conditions.script, written_block: current_blockstamp.number, }, - get_script_hash(&output.conditions.script, scripts_hash), + utxo_script_hash, )?; + // Insert on col `txs_by_recipient` + if !issuers_scripts_hashs.contains(&utxo_script_hash) { + txs_by_recipient_mem + .entry(WalletHashWithBnV1Db::new( + utxo_script_hash, + current_blockstamp.number, + )) + .or_default() + .insert(tx_hash); + } + // Increase account balance let balance = balances .get(WalletConditionsV2::from_ref(&output.conditions.script))? @@ -171,10 +178,12 @@ pub(crate) fn revert_tx<B: Backend>( mut gva_utxos, mut balances, )| { - // Remove UTXOs created by this tx use dubp::documents::transaction::TransactionDocumentTrait as _; for (output_index, output) in tx_db.tx.get_outputs().iter().enumerate() { let script = &output.conditions.script; + let utxo_script_hash = get_script_hash(&script, scripts_hash); + + // Remove UTXOs created by this tx super::utxos::remove_utxo_v10::<B>( &mut scripts_by_pubkey, &mut gva_utxos, @@ -183,9 +192,14 @@ pub(crate) fn revert_tx<B: Backend>( output_index, }, script, - get_script_hash(&script, scripts_hash), + utxo_script_hash, block_number.0, )?; + + // Remove on col `txs_by_recipient` + txs_by_recipient + .remove(WalletHashWithBnV1Db::new(utxo_script_hash, block_number)); + // Decrease account balance decrease_account_balance::<B>( script.clone(), @@ -195,7 +209,7 @@ pub(crate) fn revert_tx<B: Backend>( } // Recreate UTXOs consumed by this tx (and update balance) for input in tx_db.tx.get_inputs() { - let account_script = match input.id { + let (account_script_hash, account_script) = match input.id { SourceIdV10::Utxo(utxo_id) => { let db_tx_origin = gva_db .txs() @@ -211,6 +225,7 @@ pub(crate) fn revert_tx<B: Backend>( .conditions .script .clone(); + let utxo_script_hash = get_script_hash(&utxo_script, scripts_hash); super::utxos::write_utxo_v10::<B>( &mut scripts_by_pubkey, &mut gva_utxos, @@ -220,14 +235,20 @@ pub(crate) fn revert_tx<B: Backend>( script: &utxo_script, written_block: db_tx_origin.written_block.number, }, - get_script_hash(&utxo_script, scripts_hash), + utxo_script_hash, )?; - utxo_script + + // Return utxo_script + (utxo_script_hash, utxo_script) } SourceIdV10::Ud(UdSourceIdV10 { issuer, .. }) => { - WalletScriptV10::single_sig(issuer) + let script = WalletScriptV10::single_sig(issuer); + (Hash::compute(script.to_string().as_bytes()), script) } }; + // Remove on col `txs_by_issuer` + txs_by_issuer + .remove(WalletHashWithBnV1Db::new(account_script_hash, block_number)); // Increase account balance let balance = balances .get(WalletConditionsV2::from_ref(&account_script))? @@ -238,14 +259,10 @@ pub(crate) fn revert_tx<B: Backend>( SourceAmountValV2(balance.0 + input.amount), ); } - // Remove tx - remove_tx::<B>( - &mut txs_by_issuer, - &mut txs_by_recipient, - &mut txs, - tx_hash, - &tx_db, - )?; + + // Remove tx itself + txs.remove(HashKeyV2(*tx_hash)); + Ok(()) }, )?; @@ -256,32 +273,6 @@ pub(crate) fn revert_tx<B: Backend>( } } -fn remove_tx<B: Backend>( - txs_by_issuer: &mut TxColRw<B::Col, TxsByIssuerEvent>, - txs_by_recipient: &mut TxColRw<B::Col, TxsByRecipientEvent>, - txs: &mut TxColRw<B::Col, TxsEvent>, - tx_hash: &Hash, - tx_db: &TxDbV2, -) -> KvResult<()> { - // Remove tx hash in col `txs_by_issuer` - for pubkey in tx_db.tx.issuers() { - let mut hashs_ = txs_by_issuer.get(&PubKeyKeyV2(pubkey))?.unwrap_or_default(); - hashs_.pop(); - txs_by_issuer.upsert(PubKeyKeyV2(pubkey), hashs_) - } - // Remove tx hash in col `txs_by_recipient` - for pubkey in tx_db.tx.recipients_keys() { - let mut hashs_ = txs_by_recipient - .get(&PubKeyKeyV2(pubkey))? - .unwrap_or_default(); - hashs_.pop(); - txs_by_recipient.upsert(PubKeyKeyV2(pubkey), hashs_) - } - // Remove tx itself - txs.remove(HashKeyV2(*tx_hash)); - Ok(()) -} - fn decrease_account_balance<B: Backend>( account_script: WalletScriptV10, balances: &mut TxColRw<B::Col, BalancesEvent>, @@ -313,6 +304,7 @@ mod tests { documents::transaction::UTXOConditions, }; use duniter_dbs::BlockMetaV2; + use maplit::btreeset; #[test] fn test_apply_tx() -> KvResult<()> { @@ -336,6 +328,8 @@ mod tests { //println!("TMP pk2={}", pk2); let script = WalletScriptV10::single_sig(pk); let script2 = WalletScriptV10::single_sig(pk2); + let script_hash = Hash::compute(script.to_string().as_bytes()); + let script2_hash = Hash::compute(script2.to_string().as_bytes()); gva_db.balances_write().upsert( WalletConditionsV2(script.clone()), @@ -372,6 +366,9 @@ mod tests { let tx1_hash = tx1.get_hash(); let mut scripts_hash = HashMap::new(); + + let mut txs_by_issuer_mem = HashMap::new(); + let mut txs_by_recipient_mem = HashMap::new(); apply_tx( current_blockstamp, b0.median_time as i64, @@ -379,8 +376,21 @@ mod tests { &mut scripts_hash, tx1_hash, &tx1, + &mut txs_by_issuer_mem, + &mut txs_by_recipient_mem, )?; + assert_eq!(txs_by_issuer_mem.len(), 1); + assert_eq!( + txs_by_issuer_mem.get(&WalletHashWithBnV1Db::new(script_hash, BlockNumber(0))), + Some(&btreeset![tx1_hash]) + ); + assert_eq!(txs_by_recipient_mem.len(), 1); + assert_eq!( + txs_by_recipient_mem.get(&WalletHashWithBnV1Db::new(script2_hash, BlockNumber(0))), + Some(&btreeset![tx1_hash]) + ); + assert_eq!( gva_db .balances() @@ -417,6 +427,8 @@ mod tests { .build_and_sign(vec![kp.generate_signator()]); let tx2_hash = tx2.get_hash(); + let mut txs_by_issuer_mem = HashMap::new(); + let mut txs_by_recipient_mem = HashMap::new(); apply_tx( current_blockstamp, b0.median_time as i64, @@ -424,8 +436,21 @@ mod tests { &mut scripts_hash, tx2_hash, &tx2, + &mut txs_by_issuer_mem, + &mut txs_by_recipient_mem, )?; + assert_eq!(txs_by_issuer_mem.len(), 1); + assert_eq!( + txs_by_issuer_mem.get(&WalletHashWithBnV1Db::new(script2_hash, BlockNumber(0))), + Some(&btreeset![tx2_hash]) + ); + assert_eq!(txs_by_recipient_mem.len(), 1); + assert_eq!( + txs_by_recipient_mem.get(&WalletHashWithBnV1Db::new(script_hash, BlockNumber(0))), + Some(&btreeset![tx2_hash]) + ); + assert_eq!( gva_db .balances() diff --git a/rust-libs/modules/gva/dbs-reader/Cargo.toml b/rust-libs/modules/gva/dbs-reader/Cargo.toml index e83081ca3fc54e80515f9d1eba9c456a876358b9..b5ea2a14b3abaeec091bba8b2f7f9604d57658ce 100644 --- a/rust-libs/modules/gva/dbs-reader/Cargo.toml +++ b/rust-libs/modules/gva/dbs-reader/Cargo.toml @@ -19,4 +19,5 @@ resiter = "0.4.0" [dev-dependencies] duniter-dbs = { path = "../../../duniter-dbs", features = ["mem"] } +maplit = "1.0.2" smallvec = { version = "1.4.0", features = ["serde", "write"] } diff --git a/rust-libs/modules/gva/dbs-reader/src/lib.rs b/rust-libs/modules/gva/dbs-reader/src/lib.rs index 499f78042c6b00dd4304cb02daf214d4b5fbc8df..ca54b7f07fb7b198f003705311ec3ef233771183 100644 --- a/rust-libs/modules/gva/dbs-reader/src/lib.rs +++ b/rust-libs/modules/gva/dbs-reader/src/lib.rs @@ -50,6 +50,7 @@ use duniter_dbs::{ }; use resiter::filter::Filter; use resiter::filter_map::FilterMap; +use resiter::flatten::Flatten; use resiter::map::Map; use std::{collections::BTreeSet, str::FromStr}; diff --git a/rust-libs/modules/gva/dbs-reader/src/txs_history.rs b/rust-libs/modules/gva/dbs-reader/src/txs_history.rs index b51a23ddc38c08ff0407d56af7a1348016759f4a..36adfbc1f88e1bee9f7da326dfb65d0491fcb5e6 100644 --- a/rust-libs/modules/gva/dbs-reader/src/txs_history.rs +++ b/rust-libs/modules/gva/dbs-reader/src/txs_history.rs @@ -15,6 +15,78 @@ use crate::*; +impl DbsReader { + pub fn get_txs_history_bc_received(&self, script_hash: Hash) -> KvResult<Vec<TxDbV2>> { + let start_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(0)); + let end_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(u32::MAX)); + + self.0 + .txs_by_recipient() + .iter_ref_slice(start_k..end_k, |_k, hashs| { + let mut sent = SmallVec::<[TxDbV2; 8]>::new(); + for hash in hashs { + if let Some(tx_db) = self.0.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok() + .collect::<KvResult<Vec<_>>>() + } + pub fn get_txs_history_bc_sent(&self, script_hash: Hash) -> KvResult<Vec<TxDbV2>> { + let start_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(0)); + let end_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(u32::MAX)); + + self.0 + .txs_by_issuer() + .iter_ref_slice(start_k..end_k, |_k, hashs| { + let mut sent = SmallVec::<[TxDbV2; 8]>::new(); + for hash in hashs { + if let Some(tx_db) = self.0.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok() + .collect::<KvResult<Vec<_>>>() + } + pub fn get_txs_history_mempool<TxsMpDb: TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + pubkey: PublicKey, + ) -> KvResult<(Vec<TransactionDocumentV10>, Vec<TransactionDocumentV10>)> { + let sending = txs_mp_db_ro + .txs_by_issuer() + .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { + let mut sent = Vec::with_capacity(hashs.len()); + for hash in hashs { + if let Some(tx_db) = txs_mp_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db.0); + } + } + Ok(sent) + })? + .unwrap_or_default(); + let pending = txs_mp_db_ro + .txs_by_recipient() + .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { + let mut pending = Vec::with_capacity(hashs.len()); + for hash in hashs { + if let Some(tx_db) = txs_mp_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + pending.push(tx_db.0); + } + } + Ok(pending) + })? + .unwrap_or_default(); + Ok((sending, pending)) + } +} +use duniter_dbs::{smallvec::SmallVec, WalletHashWithBnV1Db}; + +// Needed for BMA only pub struct TxsHistory { pub sent: Vec<TxDbV2>, pub received: Vec<TxDbV2>, @@ -28,48 +100,37 @@ pub fn get_transactions_history_for_bma<GvaDb: GvaV1DbReadable, TxsMpDb: TxsMpV2 txs_mp_db_ro: &TxsMpDb, pubkey: PublicKey, ) -> KvResult<TxsHistory> { - get_transactions_history_inner(gva_db_ro, txs_mp_db_ro, pubkey) -} - -impl DbsReader { - pub fn get_transactions_history<TxsMpDb: TxsMpV2DbReadable>( - &self, - txs_mp_db_ro: &TxsMpDb, - pubkey: PublicKey, - ) -> KvResult<TxsHistory> { - get_transactions_history_inner(self.0, txs_mp_db_ro, pubkey) - } -} + let script_hash = Hash::compute(WalletScriptV10::single_sig(pubkey).to_string().as_bytes()); + let start_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(0)); + let end_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(u32::MAX)); -fn get_transactions_history_inner<GvaDb: GvaV1DbReadable, TxsMpDb: TxsMpV2DbReadable>( - gva_db_ro: &GvaDb, - txs_mp_db_ro: &TxsMpDb, - pubkey: PublicKey, -) -> KvResult<TxsHistory> { let sent = gva_db_ro .txs_by_issuer() - .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { - let mut sent = Vec::with_capacity(hashs.len()); + .iter_ref_slice(start_k..end_k, |_k, hashs| { + let mut sent = SmallVec::<[TxDbV2; 2]>::new(); for hash in hashs { if let Some(tx_db) = gva_db_ro.txs().get(HashKeyV2::from_ref(hash))? { sent.push(tx_db); } } Ok(sent) - })? - .unwrap_or_default(); + }) + .flatten_ok() + .collect::<KvResult<Vec<_>>>()?; + let received = gva_db_ro .txs_by_recipient() - .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { - let mut received = Vec::with_capacity(hashs.len()); + .iter_ref_slice(start_k..end_k, |_k, hashs| { + let mut sent = SmallVec::<[TxDbV2; 2]>::new(); for hash in hashs { if let Some(tx_db) = gva_db_ro.txs().get(HashKeyV2::from_ref(hash))? { - received.push(tx_db); + sent.push(tx_db); } } - Ok(received) - })? - .unwrap_or_default(); + Ok(sent) + }) + .flatten_ok() + .collect::<KvResult<Vec<_>>>()?; let sending = txs_mp_db_ro .txs_by_issuer() .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { @@ -101,3 +162,35 @@ fn get_transactions_history_inner<GvaDb: GvaV1DbReadable, TxsMpDb: TxsMpV2DbRead pending, }) } + +#[cfg(test)] +mod tests { + use super::*; + use dubp::crypto::keys::ed25519::PublicKey; + use duniter_dbs::databases::gva_v1::GvaV1DbWritable; + use maplit::btreeset; + + #[test] + fn test_get_txs_history_bc() -> KvResult<()> { + let gva_db = duniter_dbs::databases::gva_v1::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + + let s1 = WalletScriptV10::single_sig(PublicKey::default()); + let s1_hash = Hash::compute_str(&s1.to_string()); + + gva_db + .txs_write() + .upsert(HashKeyV2(Hash::default()), TxDbV2::default())?; + gva_db.txs_by_issuer_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(0)), + btreeset![Hash::default()], + )?; + + let _received = db_reader.get_txs_history_bc_received(s1_hash)?; + let sent = db_reader.get_txs_history_bc_sent(s1_hash)?; + + assert_eq!(sent, vec![TxDbV2::default()]); + + Ok(()) + } +} diff --git a/rust-libs/modules/gva/src/entities.rs b/rust-libs/modules/gva/src/entities.rs index 5fe069ae06ffb48be87d37f21360394dd7478fb9..0ab62d77e3c1d3244131a0dab05f8586b0768416 100644 --- a/rust-libs/modules/gva/src/entities.rs +++ b/rust-libs/modules/gva/src/entities.rs @@ -85,17 +85,21 @@ pub(crate) struct Sum { } #[derive(async_graphql::SimpleObject)] -pub(crate) struct TxsHistoryGva { - /// Transactions sent - pub(crate) sent: Vec<TxGva>, +pub(crate) struct TxsHistoryMempool { /// Transactions sending pub(crate) sending: Vec<TxGva>, - /// Transactions received - pub(crate) received: Vec<TxGva>, /// Transactions receiving pub(crate) receiving: Vec<TxGva>, } +#[derive(async_graphql::SimpleObject)] +pub(crate) struct TxsHistoryBlockchain { + /// Transactions sent + pub(crate) sent: Vec<TxGva>, + /// Transactions received + pub(crate) received: Vec<TxGva>, +} + #[derive(Clone, async_graphql::SimpleObject)] pub(crate) struct UtxoGva { /// Source amount diff --git a/rust-libs/modules/gva/src/lib.rs b/rust-libs/modules/gva/src/lib.rs index 9d868bf46450e81b0073b87ceb1e917ef578c1e1..6a6767261fc60ae1a8d9679308d12bb552ee683d 100644 --- a/rust-libs/modules/gva/src/lib.rs +++ b/rust-libs/modules/gva/src/lib.rs @@ -39,7 +39,8 @@ use crate::entities::{ block_gva::Block, tx_gva::TxGva, ud_gva::{CurrentUdGva, RevalUdGva, UdGva}, - AggregateSum, AmountWithBase, PeerCardGva, RawTxOrChanges, Sum, TxsHistoryGva, UtxoGva, + AggregateSum, AmountWithBase, PeerCardGva, RawTxOrChanges, Sum, TxsHistoryBlockchain, + TxsHistoryMempool, UtxoGva, }; use crate::inputs::{TxIssuer, TxRecipient, UdsFilter}; use crate::inputs_validators::TxCommentValidator; @@ -454,11 +455,19 @@ mod tests { &self, bc_db: &BcDb, ) -> KvResult<Option<SourceAmount>>; - fn get_transactions_history<TxsMpDb: 'static + TxsMpV2DbReadable>( + fn get_txs_history_bc_received( + &self, + script_hash: Hash, + ) -> KvResult<Vec<TxDbV2>>; + fn get_txs_history_bc_sent( + &self, + script_hash: Hash, + ) -> KvResult<Vec<TxDbV2>>; + fn get_txs_history_mempool<TxsMpDb: 'static + TxsMpV2DbReadable>( &self, txs_mp_db_ro: &TxsMpDb, pubkey: PublicKey, - ) -> KvResult<duniter_gva_dbs_reader::txs_history::TxsHistory>; + ) -> KvResult<(Vec<TransactionDocumentV10>, Vec<TransactionDocumentV10>)>; fn unspent_uds_of_pubkey<BcDb: 'static + BcV2DbReadable>( &self, bc_db: &BcDb, diff --git a/rust-libs/modules/gva/src/queries.rs b/rust-libs/modules/gva/src/queries.rs index 6427b25cad081d65a2f1bd487a6d0a685b2e6596..d6fd28551fda9cd6c7a3b14176326f3294e5cba5 100644 --- a/rust-libs/modules/gva/src/queries.rs +++ b/rust-libs/modules/gva/src/queries.rs @@ -29,7 +29,8 @@ pub struct QueryRoot( queries::account_balance::AccountBalanceQuery, queries::current_frame::CurrentFrameQuery, queries::gen_tx::GenTxsQuery, - queries::txs_history::TxsHistoryQuery, + queries::txs_history::TxsHistoryBlockchainQuery, + queries::txs_history::TxsHistoryMempoolQuery, queries::uds::UdsQuery, queries::utxos_of_script::UtxosQuery, ); diff --git a/rust-libs/modules/gva/src/queries/txs_history.rs b/rust-libs/modules/gva/src/queries/txs_history.rs index ca1f714e8f8c35bb37102fab112ce4a24964f493..59ca16b5e2e946c5b923463c965e6f76df53e92c 100644 --- a/rust-libs/modules/gva/src/queries/txs_history.rs +++ b/rust-libs/modules/gva/src/queries/txs_history.rs @@ -14,48 +14,182 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. use crate::*; +use dubp::documents_parser::wallet_script_from_str; +use futures::future::join; #[derive(Default)] -pub(crate) struct TxsHistoryQuery; +pub(crate) struct TxsHistoryBlockchainQuery; + +#[async_graphql::Object] +impl TxsHistoryBlockchainQuery { + /// Transactions history (written in blockchain) + async fn txs_history_bc( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Ed25519 public key on base 58 representation or DUBP script")] + pubkey_or_script: String, + ) -> async_graphql::Result<TxsHistoryBlockchain> { + let start_time = std::time::Instant::now(); + let script = if let Ok(pubkey) = PublicKey::from_base58(&pubkey_or_script) { + WalletScriptV10::single_sig(pubkey) + } else { + wallet_script_from_str(&pubkey_or_script)? + }; + let script_hash = Hash::compute(script.to_string().as_bytes()); + + let data = ctx.data::<SchemaData>()?; + + let (sent, received) = if ctx.look_ahead().field("sent").exists() { + let db_reader = data.dbs_reader(); + let sent_fut = data + .dbs_pool + .execute(move |_| db_reader.get_txs_history_bc_sent(script_hash)); + if ctx.look_ahead().field("received").exists() { + let db_reader = data.dbs_reader(); + let received_fut = data + .dbs_pool + .execute(move |_| db_reader.get_txs_history_bc_received(script_hash)); + let (sent_res, received_res) = join(sent_fut, received_fut).await; + (sent_res??, received_res??) + } else { + let db_reader = data.dbs_reader(); + ( + data.dbs_pool + .execute(move |_| db_reader.get_txs_history_bc_sent(script_hash)) + .await??, + vec![], + ) + } + } else if ctx.look_ahead().field("received").exists() { + let db_reader = data.dbs_reader(); + ( + vec![], + data.dbs_pool + .execute(move |_| db_reader.get_txs_history_bc_received(script_hash)) + .await??, + ) + } else { + (vec![], vec![]) + }; + + println!( + "txs_history_bc duration: {}ms", + start_time.elapsed().as_millis() + ); + + Ok(TxsHistoryBlockchain { + sent: sent.into_iter().map(|db_tx| db_tx.into()).collect(), + received: received.into_iter().map(|db_tx| db_tx.into()).collect(), + }) + } +} + +#[derive(Default)] +pub(crate) struct TxsHistoryMempoolQuery; + #[async_graphql::Object] -impl TxsHistoryQuery { - /// Transactions history - async fn transactions_history( +impl TxsHistoryMempoolQuery { + /// Transactions waiting on mempool + async fn txs_history_mp( &self, ctx: &async_graphql::Context<'_>, #[graphql(desc = "Ed25519 public key on base 58 representation")] pubkey: String, - ) -> async_graphql::Result<TxsHistoryGva> { + ) -> async_graphql::Result<TxsHistoryMempool> { let pubkey = PublicKey::from_base58(&pubkey)?; let data = ctx.data::<SchemaData>()?; let db_reader = data.dbs_reader(); - let txs_history = data + let (sending, pending) = data .dbs_pool - .execute(move |dbs| db_reader.get_transactions_history(&dbs.txs_mp_db, pubkey)) + .execute(move |dbs| db_reader.get_txs_history_mempool(&dbs.txs_mp_db, pubkey)) .await??; - Ok(TxsHistoryGva { - sent: txs_history - .sent - .into_iter() - .map(|db_tx| db_tx.into()) - .collect(), - sending: txs_history - .sending + Ok(TxsHistoryMempool { + sending: sending .into_iter() .map(|db_tx| TxGva::from(&db_tx)) .collect(), - received: txs_history - .received - .into_iter() - .map(|db_tx| db_tx.into()) - .collect(), - receiving: txs_history - .pending + receiving: pending .into_iter() .map(|db_tx| TxGva::from(&db_tx)) .collect(), }) } } + +#[cfg(test)] +mod tests { + use dubp::documents::transaction::TransactionDocumentV10; + use dubp::documents::transaction::TransactionDocumentV10Stringified; + use dubp::documents_parser::prelude::FromStringObject; + use duniter_dbs::TxDbV2; + + use crate::tests::*; + + #[tokio::test] + async fn test_txs_history_blockchain() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_get_txs_history_bc_received() + .times(1) + .returning(|_| Ok(vec![])); + dbs_reader + .expect_get_txs_history_bc_sent() + .times(1) + .returning(|_| { + let tx = TransactionDocumentV10::from_string_object( + &TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: + "0-0000000000000000000000000000000000000000000000000000000000000000" + .to_owned(), + locktime: 0, + issuers: vec![], + inputs: vec![], + unlocks: vec![], + outputs: vec![], + comment: "".to_owned(), + signatures: vec![], + hash: Some( + "0000000000000000000000000000000000000000000000000000000000000000" + .to_owned(), + ), + }, + ) + .expect("wrong tx"); + Ok(vec![TxDbV2 { + tx, + ..Default::default() + }]) + }); + let schema = create_schema(dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ + txsHistoryBc(pubkeyOrScript: "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx") { + sent { + blockstamp + } + received { + blockstamp + } + } + }"# + ) + .await?, + serde_json::json!({ + "data": { + "txsHistoryBc": { + "received": [], + "sent": [{ + "blockstamp": "0-0000000000000000000000000000000000000000000000000000000000000000", + }] + } + } + }) + ); + Ok(()) + } +} diff --git a/rust-libs/modules/gva/src/schema.rs b/rust-libs/modules/gva/src/schema.rs index 3ea5213d24ff72788b5f1fcbeecd5f2539ecbdf9..ca00003b487b4f9e3361e63c09dfd75eb4817bf7 100644 --- a/rust-libs/modules/gva/src/schema.rs +++ b/rust-libs/modules/gva/src/schema.rs @@ -29,6 +29,7 @@ pub(crate) struct SchemaData { #[cfg(not(test))] impl SchemaData { + #[inline(always)] pub fn dbs_reader(&self) -> DbsReader { self.dbs_reader } diff --git a/rust-libs/tools/kv_typed/src/from_bytes.rs b/rust-libs/tools/kv_typed/src/from_bytes.rs index 5bfe507254a7a440e37af53944ffda9b434884f3..c3c39541a52389f08244b8fdac2b3e7bb567abbf 100644 --- a/rust-libs/tools/kv_typed/src/from_bytes.rs +++ b/rust-libs/tools/kv_typed/src/from_bytes.rs @@ -99,7 +99,7 @@ where fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Err> { let layout_verified = zerocopy::LayoutVerified::<_, [T]>::new_slice(bytes) - .ok_or_else(|| StringErr("".to_owned()))?; + .ok_or_else(|| StringErr("Bytes are invalid length or alignment.".to_owned()))?; let slice = layout_verified.into_slice(); Ok(BTreeSet::from_iter(slice.iter().copied())) }