From 135ef5b98fbf81477ca5a4368dbd74af011fc9e0 Mon Sep 17 00:00:00 2001 From: dastansam Date: Sun, 11 Feb 2024 00:48:03 +0100 Subject: [PATCH] Offchain worker tests --- Cargo.lock | 1 + Cargo.toml | 1 + pallets/iso-8583/Cargo.toml | 1 + pallets/iso-8583/src/impls.rs | 19 +++--- pallets/iso-8583/src/lib.rs | 33 ++++++++-- pallets/iso-8583/src/mock.rs | 3 +- pallets/iso-8583/src/tests.rs | 120 ++++++++++++++++++++++++++++++++++ runtime/src/lib.rs | 3 + 8 files changed, 164 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04826da..5b283ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4791,6 +4791,7 @@ dependencies = [ "scale-info", "sp-core", "sp-io", + "sp-keystore", "sp-runtime", "sp-std", ] diff --git a/Cargo.toml b/Cargo.toml index f8e7256..af218c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ sp-consensus-aura = { version = "0.10.0-dev", git = "https://github.com/parityte sp-consensus-grandpa = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0", default-features = false } sp-core = { version = "21.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0", default-features = false } sp-inherents = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0", default-features = false } +sp-keystore = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0", default-features = false } sp-offchain = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } sp-runtime = { version = "24.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0", default-features = false } sp-session = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } diff --git a/pallets/iso-8583/Cargo.toml b/pallets/iso-8583/Cargo.toml index 0e961f2..fff5197 100644 --- a/pallets/iso-8583/Cargo.toml +++ b/pallets/iso-8583/Cargo.toml @@ -29,6 +29,7 @@ lite-json = { workspace = true } sp-runtime = { workspace = true, features = ["std"] } pallet-balances = { workspace = true, features = ["std", "insecure_zero_ed"] } pallet-timestamp = { workspace = true, features = ["std"] } +sp-keystore = { workspace = true, features = ["std"] } [features] default = ["std"] diff --git a/pallets/iso-8583/src/impls.rs b/pallets/iso-8583/src/impls.rs index bd02cae..b9a58ee 100644 --- a/pallets/iso-8583/src/impls.rs +++ b/pallets/iso-8583/src/impls.rs @@ -1,4 +1,5 @@ //! Implementations for the pallet. + use super::*; use crate::traits::ERC20R; use frame_support::{ @@ -6,7 +7,9 @@ use frame_support::{ pallet_prelude::DispatchResult, traits::tokens::{currency::Currency, ExistenceRequirement}, }; -use sp_runtime::{traits::TryConvert, SaturatedConversion}; + +use sp_core::crypto::Ss58Codec; +use sp_runtime::{traits::TryConvert, AccountId32, SaturatedConversion}; impl ERC20R, BalanceOf> for Pallet { fn transfer(from: &AccountIdOf, to: &AccountIdOf, value: BalanceOf) -> DispatchResult { @@ -66,9 +69,9 @@ impl TryConvert<&JsonValue, BalanceOf> for BalanceDecoder { json.clone() .to_number() .map(|num| { - let value_1 = num.integer as u128 * 10_u128.pow(num.exponent as u32 + 10); + let value_1 = num.integer as u128 * 10_u128.pow(num.exponent as u32 + 2); let value_2 = num.fraction as u128 * - 10_u128.pow(num.exponent as u32 + 10 - num.fraction_length); + 10_u128.pow(num.exponent as u32 + 2 - num.fraction_length); (value_1 + value_2).saturated_into() }) .ok_or(json) @@ -80,12 +83,10 @@ pub(crate) struct AccountIdDecoder(sp_std::marker::PhantomData); impl TryConvert<&JsonValue, AccountIdOf> for AccountIdDecoder { fn try_convert(json: &JsonValue) -> Result, &JsonValue> { - let raw_bytes = json - .clone() - .to_string() - .map(|v| v.iter().map(|c| *c as u8).collect::>()) - .ok_or(json)?; + let raw_bytes = + json.clone().to_string().map(|v| v.iter().collect::>()).ok_or(json)?; - AccountIdOf::::decode(&mut &raw_bytes[..]).map_err(|_| json) + let account_id_32 = AccountId32::from_ss58check(&raw_bytes).map_err(|_| json)?; + AccountIdOf::::decode(&mut &account_id_32.encode()[..]).map_err(|_| json) } } diff --git a/pallets/iso-8583/src/lib.rs b/pallets/iso-8583/src/lib.rs index f07da98..9838aee 100644 --- a/pallets/iso-8583/src/lib.rs +++ b/pallets/iso-8583/src/lib.rs @@ -18,11 +18,16 @@ use frame_system::{ offchain::{SendSignedTransaction, Signer}, pallet_prelude::OriginFor, }; -use sp_runtime::{offchain::http, traits::TryConvert, KeyTypeId, Saturating}; +use sp_runtime::{ + offchain::http, + traits::{TryConvert, Zero}, + KeyTypeId, Saturating, +}; use frame_support::{storage::unhashed, weights::WeightToFee}; use frame_system::{offchain::CreateSignedTransaction, pallet_prelude::*}; use lite_json::{parse_json, JsonValue, Serialize}; +use sp_std::vec; pub use pallet::*; use traits::*; @@ -44,6 +49,9 @@ mod tests; /// The keys can be inserted manually via RPC (see `author_insertKey`). pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"iso8"); +/// Max number of accounts to query in offchain worker +pub const MAX_ACCOUNTS: u32 = 20; + /// Based on the above `KeyTypeId` we need to generate a pallet-specific crypto type wrappers. /// We can use from supported crypto kinds (`sr25519`, `ed25519` and `ecdsa`) and augment /// the types with this pallet-specific identifier. @@ -106,6 +114,9 @@ pub mod pallet { type MaxStringSize: Get; /// Weight to fee conversion algorithm type WeightToFee: WeightToFee>; + /// Interval between offchain worker runs + #[pallet::constant] + type OffchainWorkerInterval: Get>; } /// Accounts registered in the oracle @@ -342,7 +353,7 @@ pub mod pallet { pub fn update_accounts( origin: OriginFor, updated_accounts: AccountsOf, - last_iterated_storage_key: StorageKey, + last_iterated_storage_key: Option, ) -> DispatchResult { // it is an unsigned transaction ensure_none(origin)?; @@ -354,7 +365,9 @@ pub mod pallet { } } - LastIteratedStorageKey::::put(last_iterated_storage_key); + if let Some(key) = last_iterated_storage_key { + LastIteratedStorageKey::::put(key); + } Ok(()) } @@ -366,7 +379,12 @@ pub mod pallet { /// /// Queries balances of all registered accounts and makes sure they are in sync with the /// offchain ledger. - fn offchain_worker(_now: BlockNumberFor) { + fn offchain_worker(now: BlockNumberFor) { + // respect interval between offchain worker runs + if now % T::OffchainWorkerInterval::get() != Zero::zero() { + return; + } + // get last iterated storage key let prefix = storage::storage_prefix( as PalletInfoAccess>::name().as_bytes(), @@ -389,7 +407,7 @@ pub mod pallet { accounts.push(account); } - if count >= 20 { + if count >= MAX_ACCOUNTS { break; } } @@ -521,7 +539,7 @@ impl Pallet { // Actually send the extrinsic to the chain let result = signer.send_signed_transaction(|_acct| Call::update_accounts { updated_accounts: updated_accounts.clone(), - last_iterated_storage_key: last_iterated_storage_key.clone(), + last_iterated_storage_key: Some(last_iterated_storage_key.clone()), }); for (acc, res) in &result { @@ -576,7 +594,7 @@ impl Pallet { log::debug!(target: "offchain-worker", "Response: {:?}", accounts); - let mut parsed_accounts = vec![]; + let mut parsed_accounts = Vec::new(); // Parse the response. Expects a list of accounts and their balances // Example response: @@ -596,6 +614,7 @@ impl Pallet { entries.len() == 2, "Invalid response, expected 2 fields" ); + let account_id = entries[0].clone(); let balance = entries[1].clone(); diff --git a/pallets/iso-8583/src/mock.rs b/pallets/iso-8583/src/mock.rs index 26f1291..ae1d5f0 100644 --- a/pallets/iso-8583/src/mock.rs +++ b/pallets/iso-8583/src/mock.rs @@ -82,7 +82,7 @@ parameter_types! { pub PalletAccount: AccountId = PalletId(*b"py/iso85").into_account_truncating(); } -type Extrinsic = TestXt; +pub(crate) type Extrinsic = TestXt; type AccountId = <::Signer as IdentifyAccount>::AccountId; impl frame_system::offchain::SigningTypes for Test { @@ -119,6 +119,7 @@ impl crate::Config for Test { type PalletAccount = PalletAccount; type MaxStringSize = ConstU32<1024>; type WeightToFee = IdentityFee; + type OffchainWorkerInterval = ConstU64<2>; } /// Mock account id for testing diff --git a/pallets/iso-8583/src/tests.rs b/pallets/iso-8583/src/tests.rs index 157279e..11ab542 100644 --- a/pallets/iso-8583/src/tests.rs +++ b/pallets/iso-8583/src/tests.rs @@ -387,3 +387,123 @@ mod trait_tests { }); } } + +mod offchain_worker { + use super::*; + use crate::{AccountsOf, Config}; + use codec::Decode; + use frame_support::traits::{Get, OffchainWorker}; + use frame_system::pallet_prelude::BlockNumberFor; + use sp_core::offchain::{testing, OffchainWorkerExt, TransactionPoolExt}; + use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; + use sp_runtime::RuntimeAppPublic; + + #[test] + fn fetch_balances_works() { + let (offchain, state) = testing::TestOffchainExt::new(); + let mut t = ExtBuilder::default().with_accounts(vec![]).build(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + let interval: BlockNumberFor = ::OffchainWorkerInterval::get(); + + // we are not expecting any request + t.execute_with(|| { + ISO8583::offchain_worker(interval - 1); + }); + + { + let mut state = state.write(); + assert_eq!(state.requests.len(), 0); + + let account_a = format!("[{:?}]", account(123).to_string()); + + // Example response: + // ```json + // [ + // {"account_id": "5GQ...","balance": 100.11}, + // {"account_id": "5FQ...","balance": 200.22}, + // .. + // ] + // ``` + let response = + format!(r#"[{{"account_id": "{}","balance": 100.11 }}]"#, account(123).to_string()); + + // prepare expectation for the request + state.expect_request(testing::PendingRequest { + method: "POST".into(), + uri: "http://localhost:3001/balances".into(), + body: account_a.into(), + response: Some(response.into()), + sent: true, + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("accept".to_string(), "*/*".to_string()), + ], + ..Default::default() + }); + } + + // skip to block `OffchainWorkerInterval` + t.execute_with(|| { + let parsed_accounts: AccountsOf = vec![(account(123), 10011)].try_into().unwrap(); + assert_eq!(ISO8583::fetch_balances(vec![account(123)]).unwrap(), parsed_accounts); + }); + } + + #[test] + fn fetch_and_submit_updated_balances_works() { + const PHRASE: &str = + "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; + + let (offchain, state) = testing::TestOffchainExt::new(); + let (pool, pool_state) = testing::TestTransactionPoolExt::new(); + let keystore = MemoryKeystore::new(); + keystore + .sr25519_generate_new(crate::crypto::Public::ID, Some(&format!("{}/iso8583", PHRASE))) + .unwrap(); + + let mut t = ExtBuilder::default().with_accounts(vec![123, 125]).build(); + t.register_extension(OffchainWorkerExt::new(offchain)); + t.register_extension(TransactionPoolExt::new(pool)); + t.register_extension(KeystoreExt::new(keystore)); + + { + let mut state = state.write(); + assert_eq!(state.requests.len(), 0); + let account_a = format!("[{:?}]", account(123).to_string()); + let response = + format!(r#"[{{"account_id": "{}","balance": 100.11 }}]"#, account(123).to_string()); + + // prepare expectation for the request + state.expect_request(testing::PendingRequest { + method: "POST".into(), + uri: "http://localhost:3001/balances".into(), + body: account_a.into(), + response: Some(response.into()), + sent: true, + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("accept".to_string(), "*/*".to_string()), + ], + ..Default::default() + }); + } + + // we are not expecting any request + t.execute_with(|| { + ISO8583::fetch_and_submit_updated_balances(vec![account(123)], vec![]).unwrap(); + + let tx = pool_state.write().transactions.pop().unwrap(); + assert!(pool_state.read().transactions.is_empty()); + let tx = crate::mock::Extrinsic::decode(&mut &tx[..]).unwrap(); + // assert_eq!(tx.signature.unwrap().0, account(123)); + assert_eq!( + tx.call, + RuntimeCall::ISO8583(crate::Call::update_accounts { + updated_accounts: vec![(account(123), 10011)].try_into().unwrap(), + last_iterated_storage_key: Some(vec![].try_into().unwrap()) + }) + ); + }); + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fe821fd..aefba6c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -260,6 +260,8 @@ impl pallet_sudo::Config for Runtime { parameter_types! { /// Pallet account ID pub PalletAccount: AccountId = PalletId(*b"py/iso85").into_account_truncating(); + /// Interval between offchain worker runs + pub const OffchainWorkerInterval: BlockNumber = 10; } impl pallet_iso_8583::Config for Runtime { @@ -268,6 +270,7 @@ impl pallet_iso_8583::Config for Runtime { type PalletAccount = PalletAccount; type MaxStringSize = ConstU32<1024>; type WeightToFee = WeightToFee; + type OffchainWorkerInterval = OffchainWorkerInterval; } // Create the runtime by composing the FRAME pallets that were previously configured.