Skip to content

Commit

Permalink
Offchain worker tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dastansam committed Feb 10, 2024
1 parent faeb1e6 commit 135ef5b
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 17 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions pallets/iso-8583/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
19 changes: 10 additions & 9 deletions pallets/iso-8583/src/impls.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
//! Implementations for the pallet.

use super::*;
use crate::traits::ERC20R;
use frame_support::{
ensure,
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<T: Config> ERC20R<AccountIdOf<T>, BalanceOf<T>> for Pallet<T> {
fn transfer(from: &AccountIdOf<T>, to: &AccountIdOf<T>, value: BalanceOf<T>) -> DispatchResult {
Expand Down Expand Up @@ -66,9 +69,9 @@ impl<T: Config> TryConvert<&JsonValue, BalanceOf<T>> for BalanceDecoder<T> {
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)
Expand All @@ -80,12 +83,10 @@ pub(crate) struct AccountIdDecoder<T: Config>(sp_std::marker::PhantomData<T>);

impl<T: Config> TryConvert<&JsonValue, AccountIdOf<T>> for AccountIdDecoder<T> {
fn try_convert(json: &JsonValue) -> Result<AccountIdOf<T>, &JsonValue> {
let raw_bytes = json
.clone()
.to_string()
.map(|v| v.iter().map(|c| *c as u8).collect::<Vec<_>>())
.ok_or(json)?;
let raw_bytes =
json.clone().to_string().map(|v| v.iter().collect::<Vec<char>>()).ok_or(json)?;

AccountIdOf::<T>::decode(&mut &raw_bytes[..]).map_err(|_| json)
let account_id_32 = AccountId32::from_ss58check(&raw_bytes).map_err(|_| json)?;
AccountIdOf::<T>::decode(&mut &account_id_32.encode()[..]).map_err(|_| json)
}
}
33 changes: 26 additions & 7 deletions pallets/iso-8583/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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.
Expand Down Expand Up @@ -106,6 +114,9 @@ pub mod pallet {
type MaxStringSize: Get<u32>;
/// Weight to fee conversion algorithm
type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
/// Interval between offchain worker runs
#[pallet::constant]
type OffchainWorkerInterval: Get<BlockNumberFor<Self>>;
}

/// Accounts registered in the oracle
Expand Down Expand Up @@ -342,7 +353,7 @@ pub mod pallet {
pub fn update_accounts(
origin: OriginFor<T>,
updated_accounts: AccountsOf<T>,
last_iterated_storage_key: StorageKey,
last_iterated_storage_key: Option<StorageKey>,
) -> DispatchResult {
// it is an unsigned transaction
ensure_none(origin)?;
Expand All @@ -354,7 +365,9 @@ pub mod pallet {
}
}

LastIteratedStorageKey::<T>::put(last_iterated_storage_key);
if let Some(key) = last_iterated_storage_key {
LastIteratedStorageKey::<T>::put(key);
}

Ok(())
}
Expand All @@ -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<T>) {
fn offchain_worker(now: BlockNumberFor<T>) {
// respect interval between offchain worker runs
if now % T::OffchainWorkerInterval::get() != Zero::zero() {
return;
}

// get last iterated storage key
let prefix = storage::storage_prefix(
<Pallet<T> as PalletInfoAccess>::name().as_bytes(),
Expand All @@ -389,7 +407,7 @@ pub mod pallet {
accounts.push(account);
}

if count >= 20 {
if count >= MAX_ACCOUNTS {
break;
}
}
Expand Down Expand Up @@ -521,7 +539,7 @@ impl<T: Config> Pallet<T> {
// 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 {
Expand Down Expand Up @@ -576,7 +594,7 @@ impl<T: Config> Pallet<T> {

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:
Expand All @@ -596,6 +614,7 @@ impl<T: Config> Pallet<T> {
entries.len() == 2,
"Invalid response, expected 2 fields"
);

let account_id = entries[0].clone();
let balance = entries[1].clone();

Expand Down
3 changes: 2 additions & 1 deletion pallets/iso-8583/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ parameter_types! {
pub PalletAccount: AccountId = PalletId(*b"py/iso85").into_account_truncating();
}

type Extrinsic = TestXt<RuntimeCall, ()>;
pub(crate) type Extrinsic = TestXt<RuntimeCall, ()>;
type AccountId = <<Signature as Verify>::Signer as IdentifyAccount>::AccountId;

impl frame_system::offchain::SigningTypes for Test {
Expand Down Expand Up @@ -119,6 +119,7 @@ impl crate::Config for Test {
type PalletAccount = PalletAccount;
type MaxStringSize = ConstU32<1024>;
type WeightToFee = IdentityFee<Balance>;
type OffchainWorkerInterval = ConstU64<2>;
}

/// Mock account id for testing
Expand Down
120 changes: 120 additions & 0 deletions pallets/iso-8583/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Test> = <Test as Config>::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<Test> = 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())
})
);
});
}
}
3 changes: 3 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down

0 comments on commit 135ef5b

Please sign in to comment.