From dc69aa14b69d7d92f5976b3912777e522e74af84 Mon Sep 17 00:00:00 2001 From: "B. Yap" <2826165+b-yap@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:07:37 +0800 Subject: [PATCH] 417 Fix 4FC-05 commented out code (#486) * first iteration * cleanup more todo's * cleanup txsets todos * https://github.com/interlay/interbtc-clients/commit/622f36b5efd538c2011ecfebce2425de6be17388 * fix https://github.com/pendulum-chain/spacewalk/issues/417#issuecomment-1943764696 * cleanup is_public_network() method and the testchain runtime cfg-if * implement the faucet_url todo * is_transaction_already_submitted * 2nd iteration to improve is_transaction_already_submitted() * revert changes of the `fn is_transaction_already_submitted()` * test prepush-hook * update `fn is_transaction_already_submitted()` * remove unnecessary files * fix resources * fix check_bump_sequence_number_and_submit() test case; update the configs * just allow it * cleanup the comments * clippy cleanup * #277 and https://github.com/pendulum-chain/spacewalk/pull/486#discussion_r1499498740, https://github.com/pendulum-chain/spacewalk/pull/486#discussion_r1499498740 * rebase and https://github.com/pendulum-chain/spacewalk/pull/486#discussion_r1499468559 * rename file for stellar_secretkey_testnet * update function `get_mainnet_secret_key` to `get_secret_key` that accepts 2 params * clippy fix * Update Cargo.lock update cargo lock --- Cargo.lock | 3 +- clients/runtime/Cargo.toml | 4 +- clients/runtime/src/rpc.rs | 15 +- clients/runtime/src/types.rs | 6 +- .../stellar-relay-lib/src/connection/error.rs | 2 - .../src/connection/xdr_converter.rs | 76 ++---- .../mainnet/stellar_secretkey_mainnet | 1 + .../stellar_secretkey_mainnet_with_currency} | 0 .../testnet/stellar_secretkey_testnet | 1 + .../stellar_secretkey_testnet_with_currency} | 0 .../vault/resources/test/tx_sets/92886_92900 | Bin 0 -> 55636 bytes .../vault/resources/test/tx_sets/92901_92915 | Bin 0 -> 51948 bytes .../vault/resources/test/tx_sets/92916_92930 | Bin 0 -> 40912 bytes .../test/tx_sets_for_testing/92931_92945 | Bin 0 -> 44912 bytes clients/vault/src/issue.rs | 6 +- clients/vault/src/oracle/agent.rs | 14 +- .../vault/src/oracle/collector/collector.rs | 77 +++--- .../src/oracle/collector/proof_builder.rs | 3 +- clients/vault/src/oracle/storage/impls.rs | 165 ++++++------ clients/vault/src/oracle/storage/traits.rs | 3 +- clients/vault/src/oracle/testing_utils.rs | 8 +- clients/vault/src/oracle/types/constants.rs | 2 +- .../src/oracle/types/double_sided_map.rs | 3 +- .../src/oracle/types/limited_fifo_map.rs | 9 +- clients/vault/src/oracle/types/types.rs | 4 +- clients/vault/src/requests/execution.rs | 4 +- clients/vault/src/requests/structs.rs | 4 +- clients/vault/src/system.rs | 66 +++-- clients/vault/tests/helper/helper.rs | 3 +- clients/vault/tests/helper/mod.rs | 12 +- .../vault/tests/vault_integration_tests.rs | 4 +- clients/wallet/src/horizon/horizon.rs | 4 +- clients/wallet/src/horizon/mod.rs | 3 - clients/wallet/src/horizon/responses.rs | 66 +++-- clients/wallet/src/horizon/tests.rs | 15 +- clients/wallet/src/resubmissions.rs | 234 +++++++++++++++--- clients/wallet/src/types.rs | 2 +- pallets/oracle/src/lib.rs | 3 +- pallets/oracle/src/tests.rs | 7 - pallets/redeem/src/benchmarking.rs | 46 ++-- pallets/replace/src/benchmarking.rs | 58 +++-- testchain/node/src/cli.rs | 2 +- testchain/runtime/testnet/src/lib.rs | 17 +- 43 files changed, 573 insertions(+), 379 deletions(-) create mode 100644 clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet rename clients/vault/resources/secretkey/{stellar_secretkey_mainnet => mainnet/stellar_secretkey_mainnet_with_currency} (100%) create mode 100644 clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet rename clients/vault/resources/secretkey/{stellar_secretkey_testnet => testnet/stellar_secretkey_testnet_with_currency} (100%) create mode 100644 clients/vault/resources/test/tx_sets/92886_92900 create mode 100644 clients/vault/resources/test/tx_sets/92901_92915 create mode 100644 clients/vault/resources/test/tx_sets/92916_92930 create mode 100644 clients/vault/resources/test/tx_sets_for_testing/92931_92945 diff --git a/Cargo.lock b/Cargo.lock index 3a0b32999..3a0297c3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10253,12 +10253,13 @@ dependencies = [ [[package]] name = "substrate-stellar-sdk" version = "0.2.4" -source = "git+https://github.com/pendulum-chain/substrate-stellar-sdk?branch=polkadot-v0.9.40#dc4212fc0b9c29d9fd370cde7ba666172de8fb7b" +source = "git+https://github.com/pendulum-chain/substrate-stellar-sdk?branch=polkadot-v0.9.40#0030e2aaedef7b7cb89108a64a7b9569a20d8044" dependencies = [ "base64 0.13.1", "hex", "lazy_static", "num-rational", + "scale-info", "serde", "serde_json", "sha2 0.9.9", diff --git a/clients/runtime/Cargo.toml b/clients/runtime/Cargo.toml index e87011441..d43a2f4ff 100644 --- a/clients/runtime/Cargo.toml +++ b/clients/runtime/Cargo.toml @@ -14,9 +14,9 @@ testing-utils = [ "tempdir", "rand", "testchain", - "testchain-runtime", + "testchain-runtime/testing-utils", "subxt-client", - "oracle" + "oracle/testing-utils" ] [dependencies] diff --git a/clients/runtime/src/rpc.rs b/clients/runtime/src/rpc.rs index a2b34c229..54018c2c3 100644 --- a/clients/runtime/src/rpc.rs +++ b/clients/runtime/src/rpc.rs @@ -1437,17 +1437,24 @@ impl ReplacePallet for SpacewalkParachain { #[async_trait] pub trait StellarRelayPallet { - async fn is_public_network(&self) -> Result; + async fn is_public_network(&self) -> bool; } #[async_trait] impl StellarRelayPallet for SpacewalkParachain { - async fn is_public_network(&self) -> Result { + async fn is_public_network(&self) -> bool { let address = metadata::constants().stellar_relay().is_public_network(); let result = self.api.constants().at(&address); match result { - Ok(result) => Ok(result), - Err(_) => Err(Error::ConstantNotFound("is_public_network".to_string())), + Ok(result) => result, + Err(e) => { + // Sometimes the fetch fails with 'StorageItemNotFound' error. + // We assume public network by default + log::warn!( + "Failed to fetch public network status from parachain: {e:?}. Assuming public network." + ); + true + }, } } } diff --git a/clients/runtime/src/types.rs b/clients/runtime/src/types.rs index 7c365b376..751575767 100644 --- a/clients/runtime/src/types.rs +++ b/clients/runtime/src/types.rs @@ -330,9 +330,9 @@ mod dispatch_error { DispatchError::Arithmetic(arithmetic_error.into()), RichDispatchError::Transactional(transactional_error) => DispatchError::Transactional(transactional_error.into()), - RichDispatchError::Exhausted | - sp_runtime::DispatchError::Corruption | - sp_runtime::DispatchError::Unavailable => todo!(), + RichDispatchError::Exhausted => DispatchError::Exhausted, + sp_runtime::DispatchError::Corruption => DispatchError::Corruption, + sp_runtime::DispatchError::Unavailable => DispatchError::Unavailable, } } } diff --git a/clients/stellar-relay-lib/src/connection/error.rs b/clients/stellar-relay-lib/src/connection/error.rs index 334600383..261963356 100644 --- a/clients/stellar-relay-lib/src/connection/error.rs +++ b/clients/stellar-relay-lib/src/connection/error.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] //todo: remove after being tested and implemented - use crate::{connection::xdr_converter::Error as XDRError, helper::error_to_string}; use substrate_stellar_sdk::{types::ErrorCode, StellarSdkError}; use tokio::sync; diff --git a/clients/stellar-relay-lib/src/connection/xdr_converter.rs b/clients/stellar-relay-lib/src/connection/xdr_converter.rs index cb25e724b..9ff390188 100644 --- a/clients/stellar-relay-lib/src/connection/xdr_converter.rs +++ b/clients/stellar-relay-lib/src/connection/xdr_converter.rs @@ -1,10 +1,8 @@ -#![allow(dead_code)] //todo: remove after being tested and implemented - use crate::sdk::types::{ AuthenticatedMessage, AuthenticatedMessageV0, HmacSha256Mac, MessageType, StellarMessage, }; use std::fmt::Debug; -use substrate_stellar_sdk::XdrCodec; +use substrate_stellar_sdk::{parse_stellar_type, StellarSdkError, XdrCodec}; #[derive(Debug, Eq, PartialEq, err_derive::Error)] pub enum Error { @@ -14,10 +12,19 @@ pub enum Error { #[error(display = "Message Version: Unsupported")] UnsupportedMessageVersion, + #[error(display = "Sdk Error: {}", _0)] + SdkError(String), + #[error(display = "Decode Error: {}", _0)] DecodeError(String), } +impl From for Error { + fn from(value: StellarSdkError) -> Self { + Error::SdkError(format!("{value:#?}")) + } +} + /// The 1st 4 bytes determines the byte length of the next stellar message. /// Returns 0 if the array of u8 is less than 4 bytes, or exceeds the max of u32. pub(crate) fn get_xdr_message_length(data: &[u8]) -> usize { @@ -37,36 +44,6 @@ pub(crate) fn from_authenticated_message(message: &AuthenticatedMessage) -> Resu message_to_bytes(message) } -// todo: move to substrate-stellar-sdk -/// To easily convert any bytes to a Stellar type. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// use substrate_stellar_sdk::types::Auth; -/// use stellar_relay_lib::parse_stellar_type; -/// let auth_xdr = [0, 0, 0, 1]; -/// let result = parse_stellar_type!(auth_xdr,Auth); -/// assert_eq!(result, Ok(Auth { flags: 1 })) -/// ``` -#[macro_export] -macro_rules! parse_stellar_type { - ($ref:ident, $struct_str:ident) => {{ - use $crate::{ - sdk::{types::$struct_str, XdrCodec}, - xdr_converter::Error, - }; - - let ret: Result<$struct_str, Error> = $struct_str::from_xdr($ref).map_err(|e| { - log::error!("decode error: {:?}", e); - Error::DecodeError(stringify!($struct_str).to_string()) - }); - ret - }}; -} - /// Parses the xdr message into `AuthenticatedMessageV0`. /// When successful, returns a tuple of the message and the `MessageType`. pub(crate) fn parse_authenticated_message( @@ -92,7 +69,7 @@ pub(crate) fn parse_authenticated_message( } fn parse_stellar_message(xdr_message: &[u8]) -> Result { - parse_stellar_type!(xdr_message, StellarMessage) + parse_stellar_type!(xdr_message, StellarMessage).map_err(|e| e.into()) } fn parse_message_version(xdr_message: &[u8]) -> Result { @@ -104,11 +81,11 @@ fn parse_sequence(xdr_message: &[u8]) -> Result { } fn parse_hmac(xdr_message: &[u8]) -> Result { - parse_stellar_type!(xdr_message, HmacSha256Mac) + parse_stellar_type!(xdr_message, HmacSha256Mac).map_err(|e| e.into()) } fn parse_message_type(xdr_message: &[u8]) -> Result { - parse_stellar_type!(xdr_message, MessageType) + parse_stellar_type!(xdr_message, MessageType).map_err(|e| e.into()) } /// Returns XDR format of the message or @@ -135,21 +112,19 @@ pub fn log_decode_error(source: &str, error: T) -> Error { Error::DecodeError(source.to_string()) } -// extra function. -fn is_xdr_complete_message(data: &[u8], message_len: usize) -> bool { - data.len() - 4 >= message_len -} - -// extra function -fn get_message(data: &[u8], message_len: usize) -> (Vec, Vec) { - (data[4..(message_len + 4)].to_owned(), data[0..(message_len + 4)].to_owned()) -} - #[cfg(test)] mod test { - use crate::connection::xdr_converter::{ - get_message, get_xdr_message_length, is_xdr_complete_message, parse_authenticated_message, - }; + use crate::connection::xdr_converter::{get_xdr_message_length, parse_authenticated_message}; + + // extra function. + fn is_xdr_complete_message(data: &[u8], message_len: usize) -> bool { + data.len() - 4 >= message_len + } + + // extra function + fn get_message(data: &[u8], message_len: usize) -> (Vec, Vec) { + (data[4..(message_len + 4)].to_owned(), data[0..(message_len + 4)].to_owned()) + } #[test] fn get_xdr_message_length_success() { @@ -165,8 +140,7 @@ mod test { base64::STANDARD ).expect("should be able to decode to bytes"); - //todo: once the authenticatedmessagev0 type is solved, continue the test - let _ = parse_authenticated_message(&msg); + assert!(parse_authenticated_message(&msg).is_ok()); } #[test] diff --git a/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet b/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet new file mode 100644 index 000000000..169351c64 --- /dev/null +++ b/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet @@ -0,0 +1 @@ +SDNQJEIRSA6YF5JNS6LQLCBF2XVWZ2NJV3YLC322RGIBJIJRIRGWKLEF \ No newline at end of file diff --git a/clients/vault/resources/secretkey/stellar_secretkey_mainnet b/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet_with_currency similarity index 100% rename from clients/vault/resources/secretkey/stellar_secretkey_mainnet rename to clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet_with_currency diff --git a/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet b/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet new file mode 100644 index 000000000..169351c64 --- /dev/null +++ b/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet @@ -0,0 +1 @@ +SDNQJEIRSA6YF5JNS6LQLCBF2XVWZ2NJV3YLC322RGIBJIJRIRGWKLEF \ No newline at end of file diff --git a/clients/vault/resources/secretkey/stellar_secretkey_testnet b/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet_with_currency similarity index 100% rename from clients/vault/resources/secretkey/stellar_secretkey_testnet rename to clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet_with_currency diff --git a/clients/vault/resources/test/tx_sets/92886_92900 b/clients/vault/resources/test/tx_sets/92886_92900 new file mode 100644 index 0000000000000000000000000000000000000000..ca331a3accaecf5e68410e857660c0cea9063793 GIT binary patch literal 55636 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 S;~x!GQp0&DP6O}*qWuS;eCXu> literal 0 HcmV?d00001 diff --git a/clients/vault/resources/test/tx_sets/92901_92915 b/clients/vault/resources/test/tx_sets/92901_92915 new file mode 100644 index 0000000000000000000000000000000000000000..bdc35b20098cb2e44cf3de40e09de978dcfce225 GIT binary patch literal 51948 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 In;Oc00VrTq|&a9 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 I5L>G7fB5QOssI20 literal 0 HcmV?d00001 diff --git a/clients/vault/resources/test/tx_sets_for_testing/92931_92945 b/clients/vault/resources/test/tx_sets_for_testing/92931_92945 new file mode 100644 index 0000000000000000000000000000000000000000..5ad4ea883b81ef3782d194d94794fbdf1753dd3b GIT binary patch literal 44912 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 XZ2GoECkI%a-gdn~vREI@tBLe~0MJ;1 literal 0 HcmV?d00001 diff --git a/clients/vault/src/issue.rs b/clients/vault/src/issue.rs index e1dc543d9..bf1ef6f3e 100644 --- a/clients/vault/src/issue.rs +++ b/clients/vault/src/issue.rs @@ -14,10 +14,7 @@ use wallet::{ types::FilterWith, LedgerTxEnvMap, Slot, SlotTask, SlotTaskStatus, TransactionResponse, }; -use crate::{ - oracle::{types::Slot as OracleSlot, OracleAgent}, - ArcRwLock, Error, Event, -}; +use crate::{oracle::OracleAgent, ArcRwLock, Error, Event}; fn is_vault(p1: &PublicKey, p2_raw: [u8; 32]) -> bool { return *p1.as_binary() == p2_raw @@ -307,7 +304,6 @@ pub async fn execute_issue( slot: Slot, sender: tokio::sync::oneshot::Sender, ) { - let slot = OracleSlot::from(slot); // Get the proof of the given slot let proof = match oracle_agent.get_proof(slot).await { diff --git a/clients/vault/src/oracle/agent.rs b/clients/vault/src/oracle/agent.rs index 55dfabac6..1b8f18fe8 100644 --- a/clients/vault/src/oracle/agent.rs +++ b/clients/vault/src/oracle/agent.rs @@ -13,11 +13,9 @@ use stellar_relay_lib::{ }; use crate::oracle::{ - collector::ScpMessageCollector, - errors::Error, - types::{Slot, StellarMessageSender}, - AddTxSet, Proof, + collector::ScpMessageCollector, errors::Error, types::StellarMessageSender, AddTxSet, Proof, }; +use wallet::Slot; pub struct OracleAgent { collector: Arc>, @@ -207,7 +205,7 @@ impl OracleAgent { #[cfg(test)] mod tests { use crate::oracle::{ - get_random_secret_key, get_test_secret_key, specific_stellar_relay_config, + get_random_secret_key, get_secret_key, specific_stellar_relay_config, traits::ArchiveStorage, ScpArchiveStorage, TransactionsArchiveStorage, }; @@ -259,7 +257,7 @@ mod tests { let shutdown_sender = ShutdownSender::new(); let agent = start_oracle_agent( specific_stellar_relay_config(true, 1), - &get_test_secret_key(true), + &get_secret_key(true, true), shutdown_sender, ) .await @@ -299,7 +297,7 @@ mod tests { let shutdown_sender = ShutdownSender::new(); let agent = - start_oracle_agent(modified_config, &get_test_secret_key(true), shutdown_sender) + start_oracle_agent(modified_config, &get_secret_key(true, true), shutdown_sender) .await .expect("Failed to start agent"); @@ -326,7 +324,7 @@ mod tests { StellarOverlayConfig { stellar_history_archive_urls: vec![], ..base_config }; let shutdown = ShutdownSender::new(); - let agent = start_oracle_agent(modified_config, &get_test_secret_key(true), shutdown) + let agent = start_oracle_agent(modified_config, &get_secret_key(true, true), shutdown) .await .expect("Failed to start agent"); diff --git a/clients/vault/src/oracle/collector/collector.rs b/clients/vault/src/oracle/collector/collector.rs index 97f014c98..c17879d4f 100644 --- a/clients/vault/src/oracle/collector/collector.rs +++ b/clients/vault/src/oracle/collector/collector.rs @@ -2,16 +2,15 @@ use std::{default::Default, sync::Arc}; use parking_lot::{lock_api::RwLockReadGuard, RawRwLock, RwLock}; -use runtime::stellar::types::GeneralizedTransactionSet; - use stellar_relay_lib::sdk::{ network::{Network, PUBLIC_NETWORK, TEST_NETWORK}, - types::{ScpEnvelope, TransactionSet}, + types::{GeneralizedTransactionSet, ScpEnvelope, TransactionSet}, TransactionSetType, }; +use wallet::Slot; use crate::oracle::types::{ - EnvelopesMap, LimitedFifoMap, Slot, TxSetHash, TxSetHashAndSlotMap, TxSetMap, + EnvelopesMap, LimitedFifoMap, TxSetHash, TxSetHashAndSlotMap, TxSetMap, }; /// Collects all ScpMessages and the TxSets. @@ -31,7 +30,7 @@ pub struct ScpMessageCollector { txset_and_slot_map: Arc>, /// The last slot with an SCPEnvelope - last_slot_index: u64, + last_slot_index: Slot, public_network: bool, @@ -236,7 +235,7 @@ mod test { collector::{collector::AddTxSet, ScpMessageCollector}, random_stellar_relay_config, traits::FileHandler, - EnvelopesFileHandler, + EnvelopesFileHandler, TxSetsFileHandler, }; fn open_file(file_name: &str) -> Vec { @@ -365,7 +364,6 @@ mod test { assert_eq!(res, 15); } - // todo: update this with a new txset sample #[test] fn remove_data_works() { let collector = ScpMessageCollector::new(false, stellar_history_archive_urls()); @@ -374,47 +372,42 @@ mod test { let env_map = EnvelopesFileHandler::get_map_from_archives(env_slot).expect("should return a map"); - // let txset_slot = 42867088; - // let txsets_map = - // TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); + let txset_slot = 92910; + let txsets_map = + TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); - // collector.watch_slot(env_slot); collector.envelopes_map.write().append(env_map); - // let txset = txsets_map.get(&txset_slot).expect("should return a tx set"); - // collector.txset_map.write().insert(env_slot, txset.clone()); + let txset = txsets_map.get(&txset_slot).expect("should return a tx set"); + collector.txset_map.write().insert(env_slot, txset.clone()); assert!(collector.envelopes_map.read().contains(&env_slot)); - //assert!(collector.txset_map.read().contains(&env_slot)); - // assert!(collector.slot_watchlist.read().contains_key(&env_slot)); + assert!(collector.txset_map.read().contains(&env_slot)); collector.remove_data(&env_slot); + collector.remove_data(&txset_slot); assert!(!collector.envelopes_map.read().contains(&env_slot)); - //assert!(!collector.txset_map.read().contains(&env_slot)); - } - - // todo: update this with a new txset sample - // #[test] - // fn is_txset_new_works() { - // let collector = ScpMessageCollector::new(false, stellar_history_archive_urls()); - // - // let txset_slot = 42867088; - // let txsets_map = - // TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); - // - // let txsets_size = txsets_map.len(); - // println!("txsets size: {}", txsets_size); - // - // collector.txset_map.write().append(txsets_map); - // - // collector.txset_and_slot_map.write().insert([0; 32], 0); - // - // // these didn't exist yet. - // assert!(collector.is_txset_new(&[1; 32], &5)); - // - // // the hash exists - // assert!(!collector.is_txset_new(&[0; 32], &6)); - // // the slot exists - // assert!(!collector.is_txset_new(&[3; 32], &txset_slot)); - // } + assert!(!collector.txset_map.read().contains(&txset_slot)); + } + + #[test] + fn is_txset_new_works() { + let collector = ScpMessageCollector::new(false, stellar_history_archive_urls()); + + let txset_slot = 92900; + let txsets_map = + TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); + + collector.txset_map.write().append(txsets_map); + + collector.txset_and_slot_map.write().insert([0; 32], 0); + + // these didn't exist yet. + assert!(collector.is_txset_new(&[1; 32], &5)); + + // the hash exists + assert!(!collector.is_txset_new(&[0; 32], &6)); + // the slot exists + assert!(!collector.is_txset_new(&[3; 32], &txset_slot)); + } } diff --git a/clients/vault/src/oracle/collector/proof_builder.rs b/clients/vault/src/oracle/collector/proof_builder.rs index 2bbc00ac0..24ca786fb 100644 --- a/clients/vault/src/oracle/collector/proof_builder.rs +++ b/clients/vault/src/oracle/collector/proof_builder.rs @@ -7,12 +7,13 @@ use stellar_relay_lib::sdk::{ types::{ScpEnvelope, ScpHistoryEntry, ScpStatementPledges, StellarMessage}, InitExt, TransactionSetType, XdrCodec, }; +use wallet::Slot; use crate::oracle::{ constants::{get_min_externalized_messages, MAX_SLOTS_TO_REMEMBER}, traits::ArchiveStorage, types::StellarMessageSender, - ScpArchiveStorage, ScpMessageCollector, Slot, TransactionsArchiveStorage, + ScpArchiveStorage, ScpMessageCollector, TransactionsArchiveStorage, }; /// Returns true if the SCP messages for a given slot are still recoverable from the overlay diff --git a/clients/vault/src/oracle/storage/impls.rs b/clients/vault/src/oracle/storage/impls.rs index e4dd33677..b86cc6f94 100644 --- a/clients/vault/src/oracle/storage/impls.rs +++ b/clients/vault/src/oracle/storage/impls.rs @@ -7,9 +7,10 @@ use stellar_relay_lib::sdk::{ }; use crate::oracle::{ - storage::traits::*, EnvelopesFileHandler, EnvelopesMap, Error, Filename, SerializedData, Slot, + storage::traits::*, EnvelopesFileHandler, EnvelopesMap, Error, Filename, SerializedData, SlotEncodedMap, TxSetMap, TxSetsFileHandler, }; +use wallet::Slot; use super::{ScpArchiveStorage, TransactionsArchiveStorage}; @@ -166,11 +167,11 @@ mod test { traits::{FileHandler, FileHandlerExt}, EnvelopesFileHandler, }, - types::Slot, - TransactionsArchiveStorage, + TransactionsArchiveStorage, TxSetsFileHandler, }; use super::ScpArchiveStorage; + use wallet::Slot; impl Default for ScpArchiveStorage { fn default() -> Self { @@ -227,37 +228,36 @@ mod test { assert_eq!(&file_name, &expected_name); } - // todo: enable after generating a new sample of txsetmap // ---------------- TESTS FOR TX SETS ----------- // finding first slot - // { - // let slot = 42867088; - // let expected_name = format!("{}_42867102", slot); - // let file_name = - // TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); - // assert_eq!(&file_name, &expected_name); - // } - // - // // finding slot in the middle of the file - // { - // let first_slot = 42867103; - // let expected_name = format!("{}_42867118", first_slot); - // let slot = first_slot + 10; - // - // let file_name = - // TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); - // assert_eq!(&file_name, &expected_name); - // } - // - // // finding slot at the end of the file - // { - // let slot = 42867134; - // let expected_name = format!("42867119_{}", slot); - // - // let file_name = - // TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); - // assert_eq!(&file_name, &expected_name); - // } + { + let slot = 92886; + let expected_name = format!("{}_92900", slot); + let file_name = + TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); + assert_eq!(&file_name, &expected_name); + } + + // finding slot in the middle of the file + { + let first_slot = 92916; + let expected_name = format!("{}_92930", first_slot); + let slot = first_slot + 10; + + let file_name = + TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); + assert_eq!(&file_name, &expected_name); + } + + // finding slot at the end of the file + { + let slot = 92915; + let expected_name = format!("92901_{}", slot); + + let file_name = + TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); + assert_eq!(&file_name, &expected_name); + } } #[test] @@ -281,16 +281,15 @@ mod test { } } - // todo: re-enable once a sample of txsetmap is available // ---------------- TEST FOR TXSETs ----------- - // { - // let first_slot = 42867119; - // let find_slot = first_slot + 15; - // let txsets_map = TxSetsFileHandler::get_map_from_archives(find_slot) - // .expect("should return txsets map"); - // - // assert!(txsets_map.get(&find_slot).is_some()); - // } + { + let first_slot = 92901; + let find_slot = first_slot + 15; + let txsets_map = TxSetsFileHandler::get_map_from_archives(find_slot) + .expect("should return txsets map"); + + assert!(txsets_map.get(&find_slot).is_some()); + } } #[test] @@ -307,18 +306,17 @@ mod test { } } - // todo: re-enable once a sample of txsetmap is created // ---------------- TEST FOR TXSETs ----------- - // { - // let slot = 42867087; - // - // match TxSetsFileHandler::get_map_from_archives(slot).expect_err("This should fail") { - // Error::Other(err_str) => { - // assert_eq!(err_str, format!("Cannot find file for slot {}", slot)) - // }, - // _ => assert!(false, "should fail"), - // } - // } + { + let slot = 92931; + + match TxSetsFileHandler::get_map_from_archives(slot).expect_err("This should fail") { + Error::Other(err_str) => { + assert_eq!(err_str, format!("Cannot find file for slot {}", slot)) + }, + _ => assert!(false, "should fail"), + } + } } #[test] @@ -358,38 +356,37 @@ mod test { } // ---------------- TEST FOR TXSETs ----------- - // { - // let first_slot = 42867151; - // let last_slot = 42867166; - // let mut path = PathBuf::new(); - // path.push("./resources/test/tx_sets_for_testing"); - // path.push(&format!("{}_{}", first_slot, last_slot)); - // println!("find file: {:?}", path); - // - // let mut file = File::open(path).expect("file should exist"); - // let mut bytes: Vec = vec![]; - // let _ = file.read_to_end(&mut bytes).expect("should be able to read until the end"); - // - // let mut txset_map = - // TxSetsFileHandler::deserialize_bytes(bytes).expect("should generate a map"); - // - // // let's remove the first_slot and last_slot in the map, so we can create a new file. - // txset_map.remove(&first_slot); - // txset_map.remove(&last_slot); - // - // let expected_filename = format!("{}_{}", first_slot + 1, last_slot - 1); - // let actual_filename = TxSetsFileHandler::write_to_file(&txset_map) - // .expect("should write to scp_envelopes directory"); - // assert_eq!(actual_filename, expected_filename); - // - // let new_file = TxSetsFileHandler::find_file_by_slot(last_slot - 2) - // .expect("should return the same file"); - // assert_eq!(new_file, expected_filename); - // - // // let's delete it - // let path = TxSetsFileHandler::get_path(&new_file); - // fs::remove_file(path).expect("should be able to remove the newly added file."); - // } + { + let first_slot = 92931; + let last_slot = 92945; + let mut path = PathBuf::new(); + path.push("./resources/test/tx_sets_for_testing"); + path.push(&format!("{}_{}", first_slot, last_slot)); + + let mut file = File::open(path).expect("file should exist"); + let mut bytes: Vec = vec![]; + let _ = file.read_to_end(&mut bytes).expect("should be able to read until the end"); + + let mut txset_map = + TxSetsFileHandler::deserialize_bytes(bytes).expect("should generate a map"); + + // let's remove the first_slot and last_slot in the map, so we can create a new file. + txset_map.remove(&first_slot); + txset_map.remove(&last_slot); + + let expected_filename = format!("{}_{}", first_slot + 1, last_slot - 1); + let actual_filename = TxSetsFileHandler::write_to_file(&txset_map) + .expect("should write to scp_envelopes directory"); + assert_eq!(actual_filename, expected_filename); + + let new_file = TxSetsFileHandler::find_file_by_slot(last_slot - 2) + .expect("should return the same file"); + assert_eq!(new_file, expected_filename); + + // let's delete it + let path = TxSetsFileHandler::get_path(&new_file); + fs::remove_file(path).expect("should be able to remove the newly added file."); + } } #[tokio::test] diff --git a/clients/vault/src/oracle/storage/traits.rs b/clients/vault/src/oracle/storage/traits.rs index ec81b5a2e..8baa029b9 100644 --- a/clients/vault/src/oracle/storage/traits.rs +++ b/clients/vault/src/oracle/storage/traits.rs @@ -11,7 +11,8 @@ use sp_core::hexdisplay::AsBytesRef; use stellar_relay_lib::sdk::{compound_types::XdrArchive, XdrCodec}; -use crate::oracle::{constants::ARCHIVE_NODE_LEDGER_BATCH, Error, Filename, SerializedData, Slot}; +use crate::oracle::{constants::ARCHIVE_NODE_LEDGER_BATCH, Error, Filename, SerializedData}; +use wallet::Slot; pub trait FileHandlerExt: FileHandler { fn create_filename_and_data(data: &T) -> Result<(Filename, SerializedData), Error>; diff --git a/clients/vault/src/oracle/testing_utils.rs b/clients/vault/src/oracle/testing_utils.rs index 6f245cd6e..bb464d4a3 100644 --- a/clients/vault/src/oracle/testing_utils.rs +++ b/clients/vault/src/oracle/testing_utils.rs @@ -43,9 +43,11 @@ fn stellar_relay_config_abs_path( .expect("should be able to extract config") } -pub fn get_test_secret_key(is_mainnet: bool) -> String { - let file_name = if is_mainnet { "mainnet" } else { "testnet" }; - let path = format!("./resources/secretkey/stellar_secretkey_{file_name}"); +pub fn get_secret_key(with_currency: bool, is_mainnet: bool) -> String { + let suffix = if with_currency { "_with_currency" } else { "" }; + let directory = if is_mainnet { "mainnet" } else { "testnet" }; + + let path = format!("./resources/secretkey/{directory}/stellar_secretkey_{directory}{suffix}"); std::fs::read_to_string(path).expect("should return a string") } diff --git a/clients/vault/src/oracle/types/constants.rs b/clients/vault/src/oracle/types/constants.rs index eff1ea20f..63edb748a 100644 --- a/clients/vault/src/oracle/types/constants.rs +++ b/clients/vault/src/oracle/types/constants.rs @@ -1,4 +1,4 @@ -use crate::oracle::types::Slot; +use wallet::Slot; /// This is for `EnvelopesMap`; how many slots is accommodated per file. /// This is used to compare against the length of the "keys", diff --git a/clients/vault/src/oracle/types/double_sided_map.rs b/clients/vault/src/oracle/types/double_sided_map.rs index f3ed26b34..a0647b342 100644 --- a/clients/vault/src/oracle/types/double_sided_map.rs +++ b/clients/vault/src/oracle/types/double_sided_map.rs @@ -1,7 +1,8 @@ #![allow(non_snake_case)] -use crate::oracle::types::{Slot, TxSetHash}; +use crate::oracle::types::TxSetHash; use std::collections::HashMap; +use wallet::Slot; /// The slot is not found in the `StellarMessage::TxSet(...)` and /// `StellarMessage::GeneralizedTxSet(...)`, therefore this map serves as a holder of the slot when diff --git a/clients/vault/src/oracle/types/limited_fifo_map.rs b/clients/vault/src/oracle/types/limited_fifo_map.rs index 9dd179529..baee598f0 100644 --- a/clients/vault/src/oracle/types/limited_fifo_map.rs +++ b/clients/vault/src/oracle/types/limited_fifo_map.rs @@ -1,9 +1,10 @@ #![allow(non_snake_case)] -use crate::oracle::{constants::DEFAULT_MAX_ITEMS_IN_QUEUE, types::Slot}; +use crate::oracle::constants::DEFAULT_MAX_ITEMS_IN_QUEUE; use itertools::Itertools; use std::{collections::VecDeque, fmt::Debug}; use stellar_relay_lib::sdk::{types::ScpEnvelope, TransactionSetType}; +use wallet::Slot; /// Sometimes not enough `StellarMessage::ScpMessage(...)` are sent per slot; /// or that the `StellarMessage::TxSet(...)` or `StellarMessage::GeneralizedTxSet(...)` @@ -68,6 +69,12 @@ where let (index, _) = self.queue.iter().find_position(|(k, _)| k == key)?; self.queue.remove(index).map(|(_, v)| v) } + + /// removes all data in the queue + pub fn clear(&mut self) { + self.queue.drain(..); + } + pub fn get(&self, key: &K) -> Option<&T> { self.queue.iter().find(|(k, _)| k == key).map(|(_, v)| v) } diff --git a/clients/vault/src/oracle/types/types.rs b/clients/vault/src/oracle/types/types.rs index 5424a2060..ada70f066 100644 --- a/clients/vault/src/oracle/types/types.rs +++ b/clients/vault/src/oracle/types/types.rs @@ -4,9 +4,9 @@ use std::collections::BTreeMap; use tokio::sync::mpsc; -use stellar_relay_lib::sdk::types::{Hash, StellarMessage, Uint64}; +use stellar_relay_lib::sdk::types::{Hash, StellarMessage}; +use wallet::Slot; -pub type Slot = Uint64; pub type TxHash = Hash; pub type TxSetHash = Hash; pub type Filename = String; diff --git a/clients/vault/src/requests/execution.rs b/clients/vault/src/requests/execution.rs index 8d0c0ab95..0c719ce52 100644 --- a/clients/vault/src/requests/execution.rs +++ b/clients/vault/src/requests/execution.rs @@ -1,6 +1,6 @@ use crate::{ error::Error, - oracle::{types::Slot, OracleAgent}, + oracle::OracleAgent, requests::{ helper::{ get_all_transactions_of_wallet_async, get_request_for_stellar_tx, @@ -23,7 +23,7 @@ use runtime::{PrettyPrint, ShutdownSender, SpacewalkParachain, UtilFuncs}; use service::{spawn_cancelable, Error as ServiceError}; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::RwLock; -use wallet::{StellarWallet, TransactionResponse}; +use wallet::{Slot, StellarWallet, TransactionResponse}; // max of 3 retries for failed request execution const MAX_EXECUTION_RETRIES: u32 = 3; diff --git a/clients/vault/src/requests/structs.rs b/clients/vault/src/requests/structs.rs index cb94f1e30..edf190bf8 100644 --- a/clients/vault/src/requests/structs.rs +++ b/clients/vault/src/requests/structs.rs @@ -1,6 +1,6 @@ use crate::{ metrics::update_stellar_metrics, - oracle::{types::Slot, OracleAgent, Proof}, + oracle::{OracleAgent, Proof}, system::VaultData, Error, }; @@ -14,7 +14,7 @@ use sp_runtime::traits::StaticLookup; use std::{convert::TryInto, sync::Arc, time::Duration}; use stellar_relay_lib::sdk::{Asset, TransactionEnvelope, XdrCodec}; use tokio::sync::RwLock; -use wallet::{StellarWallet, TransactionResponse}; +use wallet::{Slot, StellarWallet, TransactionResponse}; #[derive(Debug, Clone, PartialEq)] struct Deadline { diff --git a/clients/vault/src/system.rs b/clients/vault/src/system.rs index dca8a19ae..1a61238d3 100644 --- a/clients/vault/src/system.rs +++ b/clients/vault/src/system.rs @@ -440,11 +440,18 @@ impl VaultService { oracle_agent, self.config.payment_margin_minutes, ); + + let shutdown_clone = self.shutdown.clone(); service::spawn_cancelable(self.shutdown.subscribe(), async move { - // TODO: kill task on shutdown signal to prevent double payment - if let Err(e) = open_request_executor.await { - tracing::error!("Failed to process open requests: {}", e) - }; + match open_request_executor.await { + Ok(_) => tracing::info!("Done processing open requests"), + Err(e) => { + tracing::error!("Failed to process open requests: {}", e); + if let Err(err) = shutdown_clone.send(()) { + tracing::error!("Failed to send shutdown signal: {}", err); + } + }, + } }); } @@ -708,15 +715,7 @@ impl VaultService { monitoring_config: MonitoringConfig, shutdown: ShutdownSender, ) -> Result { - let is_public_network = - spacewalk_parachain.is_public_network().await.unwrap_or_else(|error| { - // Sometimes the fetch fails with 'StorageItemNotFound' error. - // We assume public network by default - tracing::warn!( - "Failed to fetch public network status from parachain: {error}. Assuming public network." - ); - true - }); + let is_public_network = spacewalk_parachain.is_public_network().await; let secret_key = fs::read_to_string(&config.stellar_vault_secret_key_filepath)? .trim() @@ -822,9 +821,7 @@ impl VaultService { } async fn register_public_key_if_not_present(&mut self) -> Result<(), Error> { - if let Some(_faucet_url) = &self.config.faucet_url { - // TODO fund account with faucet - } + let _ = self.try_fund_from_faucet().await; if self.spacewalk_parachain.get_public_key().await?.is_none() { let public_key = self.stellar_wallet.read().await.public_key(); @@ -841,6 +838,37 @@ impl VaultService { Ok(()) } + /// Only works when the stellar network is testnet + async fn try_fund_from_faucet(&self) -> bool { + let Some(faucet_url) = &self.config.faucet_url else { + return false + }; + + let is_public_network = self.spacewalk_parachain.is_public_network().await; + + // fund the account if on stellar TESTNET + if !is_public_network { + let account_id = self.spacewalk_parachain.get_account_id().pretty_print(); + let url = format!("{faucet_url}?to={account_id}"); + match reqwest::get(url.clone()).await { + Ok(response) if response.status().is_success() => { + tracing::info!("try_fund_from_faucet(): successful funded {account_id}"); + return true + }, + Ok(response) => { + tracing::error!("try_fund_from_faucet(): failed to fund {account_id} from faucet: {response:#?}"); + }, + Err(e) => { + tracing::error!( + "try_fund_from_faucet(): failed to fund {account_id} from faucet: {e}" + ); + }, + } + } + + false + } + async fn register_vault_with_collateral( &self, vault_id: VaultId, @@ -869,11 +897,7 @@ impl VaultService { ) .await .map_err(|e| Error::RuntimeError(e)) - } else if let Some(_faucet_url) = &self.config.faucet_url { - tracing::info!("[{}] Automatically registering...", vault_id.pretty_print()); - // TODO - // faucet::fund_and_register(&self.spacewalk_parachain, faucet_url, &vault_id) - // .await?; + } else if self.try_fund_from_faucet().await { Ok(()) } else { tracing::error!( diff --git a/clients/vault/tests/helper/helper.rs b/clients/vault/tests/helper/helper.rs index e2a4ae512..740160b32 100644 --- a/clients/vault/tests/helper/helper.rs +++ b/clients/vault/tests/helper/helper.rs @@ -9,13 +9,12 @@ use runtime::{ integration::{ assert_event, get_required_vault_collateral_for_issue, setup_provider, SubxtClient, }, - stellar::SecretKey, ExecuteRedeemEvent, IssuePallet, SpacewalkParachain, VaultId, VaultRegistryPallet, }; use sp_keyring::AccountKeyring; use sp_runtime::traits::StaticLookup; use std::{sync::Arc, time::Duration}; -use stellar_relay_lib::sdk::PublicKey; +use stellar_relay_lib::sdk::{PublicKey, SecretKey}; use vault::{oracle::OracleAgent, ArcRwLock}; use wallet::{error::Error, StellarWallet, TransactionResponse}; diff --git a/clients/vault/tests/helper/mod.rs b/clients/vault/tests/helper/mod.rs index 3e3a3bce0..1177c1b29 100644 --- a/clients/vault/tests/helper/mod.rs +++ b/clients/vault/tests/helper/mod.rs @@ -20,7 +20,7 @@ use std::{future::Future, sync::Arc}; use stellar_relay_lib::StellarOverlayConfig; use tokio::sync::RwLock; use vault::{ - oracle::{get_test_secret_key, random_stellar_relay_config, start_oracle_agent, OracleAgent}, + oracle::{get_secret_key, random_stellar_relay_config, start_oracle_agent, OracleAgent}, ArcRwLock, }; use wallet::StellarWallet; @@ -31,8 +31,9 @@ pub type StellarPublicKey = [u8; 32]; impl SpacewalkParachainExt for SpacewalkParachain {} lazy_static! { - // TODO clean this up by extending the `get_test_secret_key()` function - pub static ref DESTINATION_SECRET_KEY: String = "SDNQJEIRSA6YF5JNS6LQLCBF2XVWZ2NJV3YLC322RGIBJIJRIRGWKLEF".to_string(); + pub static ref CFG: StellarOverlayConfig = random_stellar_relay_config(false); + pub static ref SECRET_KEY: String = get_secret_key(true, false); + pub static ref DESTINATION_SECRET_KEY: String = get_secret_key(false, false); pub static ref ONE_TO_ONE_RATIO: FixedU128 = FixedU128::saturating_from_rational(1u128, 1u128); pub static ref TEN_TO_ONE_RATIO: FixedU128 = FixedU128::saturating_from_rational(1u128, 10u128); } @@ -84,8 +85,7 @@ async fn setup_chain_providers( let path = tmp_dir.path().to_str().expect("should return a string").to_string(); let stellar_config = random_stellar_relay_config(is_public_network); - let vault_stellar_secret = get_test_secret_key(is_public_network); - // TODO set destination secret key in a better way + let vault_stellar_secret = get_secret_key(true, is_public_network); let user_stellar_secret = &DESTINATION_SECRET_KEY; let (vault_wallet, user_wallet) = @@ -139,7 +139,7 @@ where ); let stellar_config = random_stellar_relay_config(is_public_network); - let vault_stellar_secret = get_test_secret_key(is_public_network); + let vault_stellar_secret = get_secret_key(true, is_public_network); let shutdown_tx = ShutdownSender::new(); let oracle_agent = diff --git a/clients/vault/tests/vault_integration_tests.rs b/clients/vault/tests/vault_integration_tests.rs index 05c7db084..98df7a4c9 100644 --- a/clients/vault/tests/vault_integration_tests.rs +++ b/clients/vault/tests/vault_integration_tests.rs @@ -23,7 +23,7 @@ mod helper; use helper::*; use primitives::DecimalsLookup; -use vault::oracle::{get_test_secret_key, random_stellar_relay_config, start_oracle_agent}; +use vault::oracle::{get_secret_key, random_stellar_relay_config, start_oracle_agent}; #[tokio::test(flavor = "multi_thread")] #[serial] @@ -662,7 +662,7 @@ async fn test_issue_execution_succeeds_from_archive() { let shutdown_tx = ShutdownSender::new(); let stellar_config = random_stellar_relay_config(is_public_network); - let vault_stellar_secret = get_test_secret_key(is_public_network); + let vault_stellar_secret = get_secret_key(true, is_public_network); // Create new oracle agent with the same configuration as the previous one let oracle_agent = start_oracle_agent(stellar_config.clone(), &vault_stellar_secret, shutdown_tx) diff --git a/clients/wallet/src/horizon/horizon.rs b/clients/wallet/src/horizon/horizon.rs index c275dc69c..3437f08eb 100644 --- a/clients/wallet/src/horizon/horizon.rs +++ b/clients/wallet/src/horizon/horizon.rs @@ -129,11 +129,11 @@ impl HorizonClient for reqwest::Client { ) -> Result { let seq_no = transaction_envelope.sequence_number(); - tracing::debug!("submitting transaction with seq no: {seq_no:?}: {transaction_envelope:?}"); - let transaction_xdr = transaction_envelope.to_base64_xdr(); let transaction_xdr = std::str::from_utf8(&transaction_xdr).map_err(Error::Utf8Error)?; + tracing::debug!("submit_transaction(): with seq no: {seq_no:?}: {transaction_xdr:?}"); + let params = [("tx", &transaction_xdr)]; let mut server_error_count = 0; diff --git a/clients/wallet/src/horizon/mod.rs b/clients/wallet/src/horizon/mod.rs index 5004d94a6..388fdb9b8 100644 --- a/clients/wallet/src/horizon/mod.rs +++ b/clients/wallet/src/horizon/mod.rs @@ -8,6 +8,3 @@ mod tests; pub use horizon::*; pub use traits::HorizonClient; - -// todo: change to Slot -pub type Ledger = u32; diff --git a/clients/wallet/src/horizon/responses.rs b/clients/wallet/src/horizon/responses.rs index eaf7e16d1..0eaef11cb 100644 --- a/clients/wallet/src/horizon/responses.rs +++ b/clients/wallet/src/horizon/responses.rs @@ -1,7 +1,8 @@ use crate::{ error::Error, - horizon::{serde::*, traits::HorizonClient, Ledger}, + horizon::{serde::*, traits::HorizonClient}, types::{FeeAttribute, PagingToken, StatusCode}, + Slot, }; use parity_scale_codec::{Decode, Encode}; use primitives::{ @@ -54,10 +55,11 @@ pub(crate) async fn interpret_response( .await .map_err(Error::HorizonResponseError)?; - let title = resp[RESPONSE_FIELD_TITLE].as_str().unwrap_or(VALUE_UNKNOWN); let status = StatusCode::try_from(resp[RESPONSE_FIELD_STATUS].as_u64().unwrap_or(400)).unwrap_or(400); + let title = resp[RESPONSE_FIELD_TITLE].as_str().unwrap_or(VALUE_UNKNOWN); + let error = match status { 400 => { let envelope_xdr = resp[RESPONSE_FIELD_EXTRAS][RESPONSE_FIELD_ENVELOPE_XDR] @@ -168,7 +170,7 @@ pub struct TransactionResponse { pub successful: bool, #[serde(deserialize_with = "de_string_to_bytes")] pub hash: Vec, - pub ledger: Ledger, + pub ledger: Slot, #[serde(deserialize_with = "de_string_to_bytes")] pub created_at: Vec, #[serde(deserialize_with = "de_string_to_bytes")] @@ -230,7 +232,7 @@ impl Debug for TransactionResponse { #[allow(dead_code)] impl TransactionResponse { - pub(crate) fn ledger(&self) -> Ledger { + pub(crate) fn ledger(&self) -> Slot { self.ledger } @@ -390,8 +392,8 @@ pub struct FeeDistribution { #[derive(Deserialize, Debug)] pub struct FeeStats { - #[serde(deserialize_with = "de_string_to_u32")] - pub last_ledger: Ledger, + #[serde(deserialize_with = "de_string_to_u64")] + pub last_ledger: Slot, #[serde(deserialize_with = "de_string_to_u32")] pub last_ledger_base_fee: u32, #[serde(deserialize_with = "de_string_to_f64")] @@ -436,7 +438,7 @@ pub struct ClaimableBalance { pub sponsor: Vec, pub claimants: Vec, - pub last_modified_ledger: Ledger, + pub last_modified_ledger: Slot, #[serde(deserialize_with = "de_string_to_bytes")] pub last_modified_time: Vec, } @@ -471,7 +473,7 @@ impl TransactionsResponseIter { } #[doc(hidden)] - // returns the first record of the list + // returns the first transaction response of the list fn get_top_record(&mut self) -> Option { if !self.is_empty() { return Some(self.records.remove(0)) @@ -479,21 +481,51 @@ impl TransactionsResponseIter { None } - /// returns the next TransactionResponse in the list + /// returns the next TransactionResponse pub async fn next(&mut self) -> Option { match self.get_top_record() { Some(record) => Some(record), None => { - // call the next page - tracing::trace!("calling next page: {}", &self.next_page); - - let response: HorizonTransactionsResponse = - self.client.get_from_url(&self.next_page).await.ok()?; - self.next_page = response.next_page(); - self.records = response.records(); - + let _ = self.jump_to_next_page().await?; self.get_top_record() }, } } + + /// returns the next TransactionResponse in reverse order + pub fn next_back(&mut self) -> Option { + self.records.pop() + } + + /// returns the TransactionResponse in the middle of the list + pub fn middle(&mut self) -> Option { + if !self.is_empty() { + let idx = self.records.len() / 2; + return Some(self.records.remove(idx)) + } + None + } + + pub fn remove_last_half_records(&mut self) { + let idx = self.records.len() / 2; + if idx != 0 { + self.records = self.records[..idx].to_vec(); + } + } + + pub fn remove_first_half_records(&mut self) { + let idx = self.records.len() / 2; + if idx != 0 { + self.records = self.records[idx..].to_vec(); + } + } + + pub async fn jump_to_next_page(&mut self) -> Option<()> { + let response: HorizonTransactionsResponse = + self.client.get_from_url(&self.next_page).await.ok()?; + self.next_page = response.next_page(); + self.records = response.records(); + + Some(()) + } } diff --git a/clients/wallet/src/horizon/tests.rs b/clients/wallet/src/horizon/tests.rs index e963ab197..09d0d3265 100644 --- a/clients/wallet/src/horizon/tests.rs +++ b/clients/wallet/src/horizon/tests.rs @@ -175,17 +175,22 @@ async fn fetch_transactions_iter_success() { let mut txs_iter = fetcher.fetch_transactions_iter(0).await.expect("should return a response"); - let next_page = txs_iter.next_page.clone(); - assert!(!next_page.is_empty()); - for _ in 0..txs_iter.records.len() { assert!(txs_iter.next().await.is_some()); } - // the list should be empty, as the last record was returned. + // the list should be empty, as the last record of this page was returned. assert_eq!(txs_iter.records.len(), 0); - // todo: when this account's # of transactions is more than 200, add a test case for it. + // if the next page + let next_page = txs_iter.next_page.clone(); + if !next_page.is_empty() { + // continue reading for transactions + assert!(txs_iter.next().await.is_some()); + + // new records can be read + assert_ne!(txs_iter.records.len(), 0); + } } #[tokio::test(flavor = "multi_thread")] diff --git a/clients/wallet/src/resubmissions.rs b/clients/wallet/src/resubmissions.rs index 9a880edd7..db4d8f1e4 100644 --- a/clients/wallet/src/resubmissions.rs +++ b/clients/wallet/src/resubmissions.rs @@ -14,13 +14,14 @@ use primitives::{ use std::time::Duration; use tokio::time::sleep; +use crate::horizon::responses::TransactionsResponseIter; #[cfg(test)] use mocktopus::macros::mockable; +use primitives::stellar::{types::SequenceNumber, PublicKey}; +use reqwest::Client; pub const RESUBMISSION_INTERVAL_IN_SECS: u64 = 1800; -const MAX_LOOK_BACK_PAGES: u8 = 10; - #[cfg_attr(test, mockable)] impl StellarWallet { pub async fn start_periodic_resubmission_of_transactions_from_cache( @@ -37,7 +38,6 @@ impl StellarWallet { tokio::spawn(async move { let me_clone = Arc::clone(&me); loop { - // Loops every 30 minutes or 1800 seconds pause_process_in_secs(interval_in_seconds).await; me_clone._resubmit_transactions_from_cache().await; @@ -193,6 +193,143 @@ impl StellarWallet { } } +fn is_source_account_match(public_key: &PublicKey, tx: &TransactionResponse) -> bool { + match tx.source_account() { + Err(_) => false, + Ok(source_account) if !source_account.eq(&public_key) => false, + _ => true, + } +} + +fn is_memo_match(tx1: &Transaction, tx2: &TransactionResponse) -> bool { + if let Some(response_memo) = tx2.memo_text() { + let Memo::MemoText(tx_memo) = &tx1.memo else { + return false + }; + + if are_memos_eq(response_memo, tx_memo.get_vec()) { + return true + } + } + false +} + +#[doc(hidden)] +/// A helper function which returns: +/// Ok(true) if both transactions match; +/// Ok(false) if the source account and the sequence number match, but NOT the MEMO; +/// Err(None) if it's absolutely NOT a match +/// Err(Some(SequenceNumber)) if the sequence number can be used for further logic checking +/// +/// # Arguments +/// +/// * `tx` - the transaction we want to confirm if it's already submitted +/// * `tx_resp` - the transaction response from Horizon +/// * `public_key` - the public key of the wallet +fn _check_transaction_match( + tx: &Transaction, + tx_resp: &TransactionResponse, + public_key: &PublicKey, +) -> Result> { + // Make sure that we are the sender and not the receiver because otherwise an + // attacker could send a transaction to us with the target memo and we'd wrongly + // assume that we already submitted this transaction. + if !is_source_account_match(public_key, &tx_resp) { + return Err(None) + } + + let Ok(source_account_sequence) = tx_resp.source_account_sequence() else { + tracing::warn!("_check_transaction_match(): cannot extract sequence number of transaction response: {tx_resp:?}"); + return Err(None) + }; + + // check if the sequence number is the same as this response + if tx.seq_num == source_account_sequence { + // Check if the transaction contains the memo we want to send + return Ok(is_memo_match(tx, &tx_resp)) + } + + Err(Some(source_account_sequence)) +} + +#[doc(hidden)] +/// A helper function which returns: +/// true if the transaction in the MIDDLE of the list is a match; +/// false if NOT a match; +/// None if a match can be found else where by narrowing down the search: +/// * if the sequence number of the transaction is < than what we're looking for, then update the +/// iterator by removing the LAST half of the list; +/// * else remove the FIRST half of the list +/// +/// # Arguments +/// +/// * `iter` - the iterator to iterate over a list of `TransactionResponse` +/// * `tx` - the transaction we want to confirm if it's already submitted +/// * `public_key` - the public key of the wallet +fn check_middle_transaction_match( + iter: &mut TransactionsResponseIter, + tx: &Transaction, + public_key: &PublicKey, +) -> Option { + let tx_sequence_num = tx.seq_num; + + let Some(response) = iter.middle() else { + return None + }; + + match _check_transaction_match(tx, &response, public_key) { + Ok(res) => return Some(res), + Err(Some(source_account_sequence)) => { + // if the sequence number is GREATER than this response, + // then a match must be in the first half of the list. + if tx_sequence_num > source_account_sequence { + iter.remove_last_half_records(); + } + // a match must be in the last half of the list. + else { + iter.remove_first_half_records(); + } + }, + _ => {}, + } + None +} + +#[doc(hidden)] +/// A helper function which returns: +/// true if the LAST transaction of the list is a match; +/// false if NOT a match; +/// None if a match can be found else where: +/// * if the sequence number of the transaction is > than what we're looking for, then update the +/// iterator by jumping to the next page; +/// * if there's no next page, then a match will never be found. Return FALSE. +async fn check_last_transaction_match( + iter: &mut TransactionsResponseIter, + tx: &Transaction, + public_key: &PublicKey, +) -> Option { + let tx_sequence_num = tx.seq_num; + let Some(response) = iter.next_back() else { + return None + }; + + match _check_transaction_match(tx, &response, public_key) { + Ok(res) => return Some(res), + Err(Some(source_account_sequence)) => { + // if the sequence number is LESSER than this response, + // then a match is possible on the NEXT page + if tx_sequence_num < source_account_sequence { + if let None = iter.jump_to_next_page().await { + // there's no pages left, meaning there's no other transactions to compare + return Some(false) + } + } + }, + _ => {}, + } + None +} + // handle tx_bad_seq #[cfg_attr(test, mockable)] impl StellarWallet { @@ -245,48 +382,54 @@ impl StellarWallet { self.submit_transaction(envelope).await } - /// This function iterates over all transactions of an account to see if a similar transaction - /// i.e. a transaction containing the same memo was already submitted previously. - /// TODO: This operation is very costly and we should try to optimize it in the future. + /// returns true if a transaction already exists and WAS submitted successfully. async fn is_transaction_already_submitted(&self, tx: &Transaction) -> bool { - // loop through until the 10th page - let mut remaining_page = 0; + let tx_sequence_num = tx.seq_num; let own_public_key = self.public_key(); - while let Ok(transaction) = self.get_all_transactions_iter().await { - if remaining_page == MAX_LOOK_BACK_PAGES { + // get the iterator + let mut iter = match self.get_all_transactions_iter().await { + Ok(iter) => iter, + Err(e) => { + tracing::warn!("is_transaction_already_submitted(): failed to get iterator: {e:?}"); + return false + }, + }; + + // iterate over the transactions, starting from + // the TOP (the largest sequence number/the latest transaction) + while let Some(response) = iter.next().await { + let top_sequence_num = match _check_transaction_match(tx, &response, &own_public_key) { + // return result for partial match + Ok(res) => return res, + // continue if it is absolutely not a match + Err(None) => continue, + // further logic checking required + Err(Some(seq_num)) => seq_num, + }; + + // if the sequence number is GREATER than this response, + // no other transaction will ever match with it. + if tx_sequence_num > top_sequence_num { break } - for response in transaction.records { - // Make sure that we are the sender and not the receiver because otherwise an - // attacker could send a transaction to us with the target memo and we'd wrongly - // assume that we already submitted this transaction. - match response.source_account() { - // no source account was found; move on to the next response - Err(_) => continue, - // the wallet's public key is not this response's source account; - // move on to the next response - Ok(source_account) if !source_account.eq(&own_public_key) => continue, - _ => {}, - } - - // Check that the transaction contains the memo that we want to send. - if let Some(response_memo) = response.memo_text() { - let Memo::MemoText(tx_memo) = &tx.memo else { - continue - }; + // check the middle response OR remove half of the responses that won't match. + if let Some(result) = check_middle_transaction_match(&mut iter, tx, &own_public_key) { + // if the middle response matched (both source account and sequence number), + // return that result + return result + } - if are_memos_eq(response_memo, tx_memo.get_vec()) { - return true - } - } + // if no match was found, check the last response OR jump to the next page + if let Some(result) = check_last_transaction_match(&mut iter, tx, &own_public_key).await + { + return result } - remaining_page += 1; + // if no match was found, continue to the next response } - // We did not find a transaction that matched our criteria false } } @@ -328,7 +471,7 @@ mod test { #[tokio::test] #[serial] async fn check_is_transaction_already_submitted() { - let wallet = wallet_with_storage("resources/check_is_transaction_already_submitted") + let wallet = wallet_with_storage("resources/checkcheck_middle_transaction_match") .expect("") .clone(); let mut wallet = wallet.write().await; @@ -357,6 +500,17 @@ mod test { assert!(wallet.is_transaction_already_submitted(&tx).await); } + // test is_transaction_already_submitted when transaction is on the next pages + { + // sequence number: 1017907249295 + let envelope = "AAAAAgAAAACI3DQX1QWOxLRQPgwS6hoKib4gD+mJIkI9QzQBT6aw7gAAAGQAAADtAAAAjwAAAAAAAAABAAAAHDExMTExMTExMTExMTExMTExMTExMTExMTExMTEAAAABAAAAAQAAAACI3DQX1QWOxLRQPgwS6hoKib4gD+mJIkI9QzQBT6aw7gAAAAEAAAAAO5kROA7+mIugqJAOsc/kTzZvfb6Ua+0HckD39iTfFcUAAAAAAAAAAAAAA+gAAAAAAAAAAU+msO4AAABAMVQZONg4CsTjVe7nmrY2LX86a7VWrmv8uL37zkqwY9Qpxte/76pUnZ/hN8o7EkpzBAWr5qb85cvzAlPgbQVGCA=="; + let tx_env = TransactionEnvelope::from_base64_xdr(envelope) + .expect("should decode into transactionenvelope"); + let tx = tx_env.get_transaction().expect("should return a transaction"); + + assert!(wallet.is_transaction_already_submitted(&tx).await); + } + // test is_transaction_already_submitted returns false { let dummy_tx = create_basic_spacewalk_stellar_transaction( @@ -401,12 +555,18 @@ mod test { let dummy_transaction = dummy_envelope.get_transaction().expect("must return a transaction"); - let _ = wallet + let resp = wallet .bump_sequence_number_and_submit(dummy_transaction.clone()) .await .expect("return ok"); + let new_dummy_transaction = + String::from_utf8(resp.envelope_xdr).expect("should return a String"); + let new_dummy_env = TransactionEnvelope::from_base64_xdr(new_dummy_transaction) + .expect("should return an envelope"); + let new_dummy_transaction = + new_dummy_env.get_transaction().expect("should return a transaction"); - assert!(wallet.is_transaction_already_submitted(&dummy_transaction).await); + assert!(wallet.is_transaction_already_submitted(&new_dummy_transaction).await); } // test bump_sequence_number_and_submit failed diff --git a/clients/wallet/src/types.rs b/clients/wallet/src/types.rs index 1f7e7a2ed..cadf11bb4 100644 --- a/clients/wallet/src/types.rs +++ b/clients/wallet/src/types.rs @@ -3,7 +3,7 @@ use primitives::stellar::TransactionEnvelope; use std::{collections::HashMap, fmt, fmt::Formatter}; pub type PagingToken = u128; -pub type Slot = u32; +pub type Slot = u64; pub type StatusCode = u16; pub type LedgerTxEnvMap = HashMap; diff --git a/pallets/oracle/src/lib.rs b/pallets/oracle/src/lib.rs index f52409c22..22fe14e0f 100644 --- a/pallets/oracle/src/lib.rs +++ b/pallets/oracle/src/lib.rs @@ -418,8 +418,7 @@ impl Pallet { >::get() } - /// TODO - /// Set the current exchange rate. ONLY FOR TESTING. + /// Set the current exchange rate. /// /// # Arguments /// diff --git a/pallets/oracle/src/tests.rs b/pallets/oracle/src/tests.rs index 8d0f6cf1a..7a12a8a5d 100644 --- a/pallets/oracle/src/tests.rs +++ b/pallets/oracle/src/tests.rs @@ -301,13 +301,6 @@ fn test_is_invalidated() { assert_ok!(Oracle::feed_values(3, vec![(key.clone(), rate)])); mine_block(); - - // max delay is 60 minutes, 60+ passed - // assert!(Oracle::is_outdated(&key, now + 3601));//TODO - - // max delay is 60 minutes, 30 passed - Oracle::get_current_time.mock_safe(move || MockResult::Return(now + 1800)); - // assert!(!Oracle::is_outdated(&key, now + 3599)); //TODO }); } diff --git a/pallets/redeem/src/benchmarking.rs b/pallets/redeem/src/benchmarking.rs index 23995ba43..3ca45806a 100644 --- a/pallets/redeem/src/benchmarking.rs +++ b/pallets/redeem/src/benchmarking.rs @@ -62,22 +62,19 @@ fn mint_wrapped(account_id: &T::AccountId, amount: BalanceOf() { let oracle_id: T::AccountId = account("Oracle", 12, 0); - use primitives::oracle::Key; - let result = Oracle::::feed_values( + Oracle::::_set_exchange_rate( + oracle_id.clone(), + get_collateral_currency_id::(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); + + Oracle::::_set_exchange_rate( oracle_id, - vec![ - ( - Key::ExchangeRate(get_collateral_currency_id::()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ( - Key::ExchangeRate(get_wrapped_currency_id()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ], - ); - assert_ok!(result); - Oracle::::begin_block(0u32.into()); + get_wrapped_currency_id(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); } fn test_request(vault_id: &DefaultVaultId) -> DefaultRedeemRequest { @@ -112,6 +109,7 @@ benchmarks! { let asset = CurrencyId::XCM(0); let stellar_address = DEFAULT_STELLAR_PUBLIC_KEY; + Security::::set_active_block_number(1u32.into()); initialize_oracle::(); register_public_key::(vault_id.clone()); @@ -135,12 +133,17 @@ benchmarks! { }: _(RawOrigin::Signed(origin), amount, stellar_address, vault_id) liquidation_redeem { - initialize_oracle::(); - let origin: T::AccountId = account("Origin", 0, 0); let vault_id = get_vault_id::(); let amount = 1000; + mint_wrapped::(&origin, amount.into()); + + mint_collateral::(&vault_id.account_id, 100_000u32.into()); + + Security::::set_active_block_number(1u32.into()); + initialize_oracle::(); + register_public_key::(vault_id.clone()); VaultRegistry::::insert_vault( @@ -148,9 +151,6 @@ benchmarks! { Vault::new(vault_id.clone()) ); - mint_wrapped::(&origin, amount.into()); - - mint_collateral::(&vault_id.account_id, 100_000u32.into()); assert_ok!(VaultRegistry::::try_deposit_collateral(&vault_id, &collateral(100_000))); assert_ok!(VaultRegistry::::try_increase_to_be_issued_tokens(&vault_id, &wrapped(amount))); @@ -168,6 +168,8 @@ benchmarks! { let vault_id = get_vault_id::(); let relayer_id: T::AccountId = account("Relayer", 0, 0); + + Security::::set_active_block_number(1u32.into()); initialize_oracle::(); let origin_stellar_address = DEFAULT_STELLAR_PUBLIC_KEY; @@ -189,8 +191,6 @@ benchmarks! { vault ); - Security::::set_active_block_number(1u32.into()); - let (validators, organizations) = get_validators_and_organizations::(); let enactment_block_height = T::BlockNumber::default(); StellarRelay::::_update_tier_1_validator_set(validators, organizations, enactment_block_height).unwrap(); @@ -302,8 +302,6 @@ benchmarks! { let vault_id = get_vault_id::(); let relayer_id: T::AccountId = account("Relayer", 0, 0); - initialize_oracle::(); - let origin_stellar_address = DEFAULT_STELLAR_PUBLIC_KEY; let redeem_id = H256::zero(); diff --git a/pallets/replace/src/benchmarking.rs b/pallets/replace/src/benchmarking.rs index 25178cdd8..d960d53f1 100644 --- a/pallets/replace/src/benchmarking.rs +++ b/pallets/replace/src/benchmarking.rs @@ -10,7 +10,7 @@ use currency::{ getters::{get_relay_chain_currency_id as get_collateral_currency_id, *}, testing_constants::get_wrapped_currency_id, }; -use oracle::{OracleKey, Pallet as Oracle}; +use oracle::Pallet as Oracle; use primitives::{CurrencyId, VaultCurrencyPair, VaultId}; use security::Pallet as Security; use stellar_relay::{ @@ -21,7 +21,7 @@ use stellar_relay::{ Config as StellarRelayConfig, Pallet as StellarRelay, }; use vault_registry::{ - types::{DefaultVaultCurrencyPair, Vault}, + types::{DefaultVault, DefaultVaultCurrencyPair, Vault}, Pallet as VaultRegistry, }; @@ -59,25 +59,26 @@ fn mint_collateral(account_id: &T::AccountId, amount: BalanceO fn initialize_oracle() { let oracle_id: T::AccountId = account("Oracle", 12, 0); - let result = Oracle::::feed_values( + Oracle::::_set_exchange_rate( + oracle_id.clone(), + get_collateral_currency_id::(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); + + Oracle::::_set_exchange_rate( + oracle_id.clone(), + get_native_currency_id::(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); + + Oracle::::_set_exchange_rate( oracle_id, - vec![ - ( - OracleKey::ExchangeRate(get_collateral_currency_id::()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ( - OracleKey::ExchangeRate(get_native_currency_id::()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ( - OracleKey::ExchangeRate(get_wrapped_currency_id()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ], - ); - assert_ok!(result); - Oracle::::begin_block(0u32.into()); + get_wrapped_currency_id(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); } fn test_request( @@ -114,11 +115,13 @@ fn register_vault(vault_id: DefaultVaultId) { benchmarks! { request_replace { - initialize_oracle::(); let vault_id = get_vault_id::("Vault"); mint_collateral::(&vault_id.account_id, (1u32 << 31).into()); let amount = Replace::::minimum_transfer_amount(get_wrapped_currency_id()).amount() + 1000_0000u32.into(); + Security::::set_active_block_number(1u32.into()); + + initialize_oracle::(); register_public_key::(vault_id.clone()); let vault = Vault { @@ -136,11 +139,13 @@ benchmarks! { }: _(RawOrigin::Signed(vault_id.account_id.clone()), vault_id.currencies.clone(), amount) withdraw_replace { - initialize_oracle::(); let vault_id = get_vault_id::("OldVault"); mint_collateral::(&vault_id.account_id, (1u32 << 31).into()); let amount = wrapped(5); + Security::::set_active_block_number(1u32.into()); + + initialize_oracle::(); let threshold = UnsignedFixedPoint::::one(); VaultRegistry::::_set_secure_collateral_threshold(get_currency_pair::(), threshold); VaultRegistry::::_set_system_collateral_ceiling(get_currency_pair::(), 1_000_000_000u32.into()); @@ -151,8 +156,15 @@ benchmarks! { VaultRegistry::::issue_tokens(&vault_id, &amount).unwrap(); VaultRegistry::::try_increase_to_be_replaced_tokens(&vault_id, &amount).unwrap(); - // TODO: check that an amount was actually withdrawn + let vault : DefaultVault:: = VaultRegistry::::get_vault_from_id(&vault_id).expect("should return a vault"); + let to_be_replaced_tokens = vault.to_be_replaced_tokens; }: _(RawOrigin::Signed(vault_id.account_id.clone()), vault_id.currencies.clone(), amount.amount()) + verify { + let vault : DefaultVault:: = VaultRegistry::::get_vault_from_id(&vault_id).expect("should return a vault"); + let updated_to_be_replaced_tokens = vault.to_be_replaced_tokens; + + assert!(to_be_replaced_tokens > updated_to_be_replaced_tokens); + } accept_replace { initialize_oracle::(); diff --git a/testchain/node/src/cli.rs b/testchain/node/src/cli.rs index 58069092e..15ceaa062 100644 --- a/testchain/node/src/cli.rs +++ b/testchain/node/src/cli.rs @@ -10,7 +10,7 @@ pub struct Cli { } #[derive(Debug, clap::Subcommand)] -#[allow(clippy::large_enum_variant)] //todo: fix large size difference between variants +#[allow(clippy::large_enum_variant)] pub enum Subcommand { /// Key management cli utilities #[command(subcommand)] diff --git a/testchain/runtime/testnet/src/lib.rs b/testchain/runtime/testnet/src/lib.rs index 82a0d95b2..899fa6005 100644 --- a/testchain/runtime/testnet/src/lib.rs +++ b/testchain/runtime/testnet/src/lib.rs @@ -448,16 +448,13 @@ impl NativeCurrencyKey for SpacewalkNativeCurrencyKey { // because this is used in the benchmark_utils::DataCollector when feeding prices impl XCMCurrencyConversion for SpacewalkNativeCurrencyKey { fn convert_to_dia_currency_id(token_symbol: u8) -> Option<(Vec, Vec)> { - // todo: this code results in Execution error: - // todo: \"Unable to get required collateral for amount\": - // todo: Module(ModuleError { index: 19, error: [0, 0, 0, 0], message: None })", data: None - // } cfg_if::cfg_if! { - // if #[cfg(not(feature = "testing-utils"))] { - // if token_symbol == 0 { - // return Some((b"Kusama".to_vec(), b"KSM".to_vec())) - // } - // } - // } + cfg_if::cfg_if! { + if #[cfg(not(feature = "testing-utils"))] { + if token_symbol == 0 { + return Some((b"Kusama".to_vec(), b"KSM".to_vec())) + } + } + } // We assume that the blockchain is always 0 and the symbol represents the token symbol let blockchain = vec![0u8]; let symbol = vec![token_symbol];