mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 13:27:57 +00:00
Move all example pallets under examples folder. (#10215)
* Put all examples under one folder Signed-off-by: Jimmy Chu <jimmychu0807@gmail.com> * Updated Cargo.toml Signed-off-by: Jimmy Chu <jimmychu0807@gmail.com> * updated for ci script Signed-off-by: Jimmy Chu <jimmychu0807@gmail.com> * update Signed-off-by: Jimmy Chu <jimmychu0807@gmail.com> * Added notes that example pallets are not meant to be used in production. Signed-off-by: Jimmy Chu <jimmychu0807@gmail.com> * updated Signed-off-by: Jimmy Chu <jimmychu0807@gmail.com>
This commit is contained in:
@@ -0,0 +1,725 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! <!-- markdown-link-check-disable -->
|
||||
//! # Offchain Worker Example Pallet
|
||||
//!
|
||||
//! The Offchain Worker Example: A simple pallet demonstrating
|
||||
//! concepts, APIs and structures common to most offchain workers.
|
||||
//!
|
||||
//! Run `cargo doc --package pallet-example-offchain-worker --open` to view this module's
|
||||
//! documentation.
|
||||
//!
|
||||
//! - [`Config`]
|
||||
//! - [`Call`]
|
||||
//! - [`Pallet`]
|
||||
//!
|
||||
//! **This pallet serves as an example showcasing Substrate off-chain worker and is not meant to
|
||||
//! be used in production.**
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! In this example we are going to build a very simplistic, naive and definitely NOT
|
||||
//! production-ready oracle for BTC/USD price.
|
||||
//! Offchain Worker (OCW) will be triggered after every block, fetch the current price
|
||||
//! and prepare either signed or unsigned transaction to feed the result back on chain.
|
||||
//! The on-chain logic will simply aggregate the results and store last `64` values to compute
|
||||
//! the average price.
|
||||
//! Additional logic in OCW is put in place to prevent spamming the network with both signed
|
||||
//! and unsigned transactions, and custom `UnsignedValidator` makes sure that there is only
|
||||
//! one unsigned transaction floating in the network.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::traits::Get;
|
||||
use frame_system::{
|
||||
self as system,
|
||||
offchain::{
|
||||
AppCrypto, CreateSignedTransaction, SendSignedTransaction, SendUnsignedTransaction,
|
||||
SignedPayload, Signer, SigningTypes, SubmitTransaction,
|
||||
},
|
||||
};
|
||||
use lite_json::json::JsonValue;
|
||||
use sp_core::crypto::KeyTypeId;
|
||||
use sp_runtime::{
|
||||
offchain::{
|
||||
http,
|
||||
storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
|
||||
Duration,
|
||||
},
|
||||
traits::Zero,
|
||||
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
|
||||
RuntimeDebug,
|
||||
};
|
||||
use sp_std::vec::Vec;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Defines application identifier for crypto keys of this module.
|
||||
///
|
||||
/// Every module that deals with signatures needs to declare its unique identifier for
|
||||
/// its crypto keys.
|
||||
/// When offchain worker is signing transactions it's going to request keys of type
|
||||
/// `KeyTypeId` from the keystore and use the ones it finds to sign the transaction.
|
||||
/// The keys can be inserted manually via RPC (see `author_insertKey`).
|
||||
pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"btc!");
|
||||
|
||||
/// 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.
|
||||
pub mod crypto {
|
||||
use super::KEY_TYPE;
|
||||
use sp_core::sr25519::Signature as Sr25519Signature;
|
||||
use sp_runtime::{
|
||||
app_crypto::{app_crypto, sr25519},
|
||||
traits::Verify,
|
||||
MultiSignature, MultiSigner,
|
||||
};
|
||||
app_crypto!(sr25519, KEY_TYPE);
|
||||
|
||||
pub struct TestAuthId;
|
||||
|
||||
impl frame_system::offchain::AppCrypto<MultiSigner, MultiSignature> for TestAuthId {
|
||||
type RuntimeAppPublic = Public;
|
||||
type GenericSignature = sp_core::sr25519::Signature;
|
||||
type GenericPublic = sp_core::sr25519::Public;
|
||||
}
|
||||
|
||||
// implemented for mock runtime in test
|
||||
impl frame_system::offchain::AppCrypto<<Sr25519Signature as Verify>::Signer, Sr25519Signature>
|
||||
for TestAuthId
|
||||
{
|
||||
type RuntimeAppPublic = Public;
|
||||
type GenericSignature = sp_core::sr25519::Signature;
|
||||
type GenericPublic = sp_core::sr25519::Public;
|
||||
}
|
||||
}
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
/// This pallet's configuration trait
|
||||
#[pallet::config]
|
||||
pub trait Config: CreateSignedTransaction<Call<Self>> + frame_system::Config {
|
||||
/// The identifier type for an offchain worker.
|
||||
type AuthorityId: AppCrypto<Self::Public, Self::Signature>;
|
||||
|
||||
/// The overarching event type.
|
||||
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
|
||||
|
||||
/// The overarching dispatch call type.
|
||||
type Call: From<Call<Self>>;
|
||||
|
||||
// Configuration parameters
|
||||
|
||||
/// A grace period after we send transaction.
|
||||
///
|
||||
/// To avoid sending too many transactions, we only attempt to send one
|
||||
/// every `GRACE_PERIOD` blocks. We use Local Storage to coordinate
|
||||
/// sending between distinct runs of this offchain worker.
|
||||
#[pallet::constant]
|
||||
type GracePeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// Number of blocks of cooldown after unsigned transaction is included.
|
||||
///
|
||||
/// This ensures that we only accept unsigned transactions once, every `UnsignedInterval`
|
||||
/// blocks.
|
||||
#[pallet::constant]
|
||||
type UnsignedInterval: Get<Self::BlockNumber>;
|
||||
|
||||
/// A configuration for base priority of unsigned transactions.
|
||||
///
|
||||
/// This is exposed so that it can be tuned for particular runtime, when
|
||||
/// multiple pallets send unsigned transactions.
|
||||
#[pallet::constant]
|
||||
type UnsignedPriority: Get<TransactionPriority>;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
/// Offchain Worker entry point.
|
||||
///
|
||||
/// By implementing `fn offchain_worker` you declare a new offchain worker.
|
||||
/// This function will be called when the node is fully synced and a new best block is
|
||||
/// succesfuly imported.
|
||||
/// Note that it's not guaranteed for offchain workers to run on EVERY block, there might
|
||||
/// be cases where some blocks are skipped, or for some the worker runs twice (re-orgs),
|
||||
/// so the code should be able to handle that.
|
||||
/// You can use `Local Storage` API to coordinate runs of the worker.
|
||||
fn offchain_worker(block_number: T::BlockNumber) {
|
||||
// Note that having logs compiled to WASM may cause the size of the blob to increase
|
||||
// significantly. You can use `RuntimeDebug` custom derive to hide details of the types
|
||||
// in WASM. The `sp-api` crate also provides a feature `disable-logging` to disable
|
||||
// all logging and thus, remove any logging from the WASM.
|
||||
log::info!("Hello World from offchain workers!");
|
||||
|
||||
// Since off-chain workers are just part of the runtime code, they have direct access
|
||||
// to the storage and other included pallets.
|
||||
//
|
||||
// We can easily import `frame_system` and retrieve a block hash of the parent block.
|
||||
let parent_hash = <system::Pallet<T>>::block_hash(block_number - 1u32.into());
|
||||
log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash);
|
||||
|
||||
// It's a good practice to keep `fn offchain_worker()` function minimal, and move most
|
||||
// of the code to separate `impl` block.
|
||||
// Here we call a helper function to calculate current average price.
|
||||
// This function reads storage entries of the current state.
|
||||
let average: Option<u32> = Self::average_price();
|
||||
log::debug!("Current price: {:?}", average);
|
||||
|
||||
// For this example we are going to send both signed and unsigned transactions
|
||||
// depending on the block number.
|
||||
// Usually it's enough to choose one or the other.
|
||||
let should_send = Self::choose_transaction_type(block_number);
|
||||
let res = match should_send {
|
||||
TransactionType::Signed => Self::fetch_price_and_send_signed(),
|
||||
TransactionType::UnsignedForAny =>
|
||||
Self::fetch_price_and_send_unsigned_for_any_account(block_number),
|
||||
TransactionType::UnsignedForAll =>
|
||||
Self::fetch_price_and_send_unsigned_for_all_accounts(block_number),
|
||||
TransactionType::Raw => Self::fetch_price_and_send_raw_unsigned(block_number),
|
||||
TransactionType::None => Ok(()),
|
||||
};
|
||||
if let Err(e) = res {
|
||||
log::error!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A public part of the pallet.
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Submit new price to the list.
|
||||
///
|
||||
/// This method is a public function of the module and can be called from within
|
||||
/// a transaction. It appends given `price` to current list of prices.
|
||||
/// In our example the `offchain worker` will create, sign & submit a transaction that
|
||||
/// calls this function passing the price.
|
||||
///
|
||||
/// The transaction needs to be signed (see `ensure_signed`) check, so that the caller
|
||||
/// pays a fee to execute it.
|
||||
/// This makes sure that it's not easy (or rather cheap) to attack the chain by submitting
|
||||
/// excesive transactions, but note that it doesn't ensure the price oracle is actually
|
||||
/// working and receives (and provides) meaningful data.
|
||||
/// This example is not focused on correctness of the oracle itself, but rather its
|
||||
/// purpose is to showcase offchain worker capabilities.
|
||||
#[pallet::weight(0)]
|
||||
pub fn submit_price(origin: OriginFor<T>, price: u32) -> DispatchResultWithPostInfo {
|
||||
// Retrieve sender of the transaction.
|
||||
let who = ensure_signed(origin)?;
|
||||
// Add the price to the on-chain list.
|
||||
Self::add_price(who, price);
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
/// Submit new price to the list via unsigned transaction.
|
||||
///
|
||||
/// Works exactly like the `submit_price` function, but since we allow sending the
|
||||
/// transaction without a signature, and hence without paying any fees,
|
||||
/// we need a way to make sure that only some transactions are accepted.
|
||||
/// This function can be called only once every `T::UnsignedInterval` blocks.
|
||||
/// Transactions that call that function are de-duplicated on the pool level
|
||||
/// via `validate_unsigned` implementation and also are rendered invalid if
|
||||
/// the function has already been called in current "session".
|
||||
///
|
||||
/// It's important to specify `weight` for unsigned calls as well, because even though
|
||||
/// they don't charge fees, we still don't want a single block to contain unlimited
|
||||
/// number of such transactions.
|
||||
///
|
||||
/// This example is not focused on correctness of the oracle itself, but rather its
|
||||
/// purpose is to showcase offchain worker capabilities.
|
||||
#[pallet::weight(0)]
|
||||
pub fn submit_price_unsigned(
|
||||
origin: OriginFor<T>,
|
||||
_block_number: T::BlockNumber,
|
||||
price: u32,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
// This ensures that the function can only be called via unsigned transaction.
|
||||
ensure_none(origin)?;
|
||||
// Add the price to the on-chain list, but mark it as coming from an empty address.
|
||||
Self::add_price(Default::default(), price);
|
||||
// now increment the block number at which we expect next unsigned transaction.
|
||||
let current_block = <system::Pallet<T>>::block_number();
|
||||
<NextUnsignedAt<T>>::put(current_block + T::UnsignedInterval::get());
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
#[pallet::weight(0)]
|
||||
pub fn submit_price_unsigned_with_signed_payload(
|
||||
origin: OriginFor<T>,
|
||||
price_payload: PricePayload<T::Public, T::BlockNumber>,
|
||||
_signature: T::Signature,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
// This ensures that the function can only be called via unsigned transaction.
|
||||
ensure_none(origin)?;
|
||||
// Add the price to the on-chain list, but mark it as coming from an empty address.
|
||||
Self::add_price(Default::default(), price_payload.price);
|
||||
// now increment the block number at which we expect next unsigned transaction.
|
||||
let current_block = <system::Pallet<T>>::block_number();
|
||||
<NextUnsignedAt<T>>::put(current_block + T::UnsignedInterval::get());
|
||||
Ok(().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Events for the pallet.
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// Event generated when new price is accepted to contribute to the average.
|
||||
/// \[price, who\]
|
||||
NewPrice(u32, T::AccountId),
|
||||
}
|
||||
|
||||
#[pallet::validate_unsigned]
|
||||
impl<T: Config> ValidateUnsigned for Pallet<T> {
|
||||
type Call = Call<T>;
|
||||
|
||||
/// Validate unsigned call to this module.
|
||||
///
|
||||
/// By default unsigned transactions are disallowed, but implementing the validator
|
||||
/// here we make sure that some particular calls (the ones produced by offchain worker)
|
||||
/// are being whitelisted and marked as valid.
|
||||
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
|
||||
// Firstly let's check that we call the right function.
|
||||
if let Call::submit_price_unsigned_with_signed_payload {
|
||||
price_payload: ref payload,
|
||||
ref signature,
|
||||
} = call
|
||||
{
|
||||
let signature_valid =
|
||||
SignedPayload::<T>::verify::<T::AuthorityId>(payload, signature.clone());
|
||||
if !signature_valid {
|
||||
return InvalidTransaction::BadProof.into()
|
||||
}
|
||||
Self::validate_transaction_parameters(&payload.block_number, &payload.price)
|
||||
} else if let Call::submit_price_unsigned { block_number, price: new_price } = call {
|
||||
Self::validate_transaction_parameters(block_number, new_price)
|
||||
} else {
|
||||
InvalidTransaction::Call.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A vector of recently submitted prices.
|
||||
///
|
||||
/// This is used to calculate average price, should have bounded size.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn prices)]
|
||||
pub(super) type Prices<T: Config> = StorageValue<_, Vec<u32>, ValueQuery>;
|
||||
|
||||
/// Defines the block when next unsigned transaction will be accepted.
|
||||
///
|
||||
/// To prevent spam of unsigned (and unpayed!) transactions on the network,
|
||||
/// we only allow one transaction every `T::UnsignedInterval` blocks.
|
||||
/// This storage entry defines when new transaction is going to be accepted.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn next_unsigned_at)]
|
||||
pub(super) type NextUnsignedAt<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;
|
||||
}
|
||||
|
||||
/// Payload used by this example crate to hold price
|
||||
/// data required to submit a transaction.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, scale_info::TypeInfo)]
|
||||
pub struct PricePayload<Public, BlockNumber> {
|
||||
block_number: BlockNumber,
|
||||
price: u32,
|
||||
public: Public,
|
||||
}
|
||||
|
||||
impl<T: SigningTypes> SignedPayload<T> for PricePayload<T::Public, T::BlockNumber> {
|
||||
fn public(&self) -> T::Public {
|
||||
self.public.clone()
|
||||
}
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
Signed,
|
||||
UnsignedForAny,
|
||||
UnsignedForAll,
|
||||
Raw,
|
||||
None,
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Chooses which transaction type to send.
|
||||
///
|
||||
/// This function serves mostly to showcase `StorageValue` helper
|
||||
/// and local storage usage.
|
||||
///
|
||||
/// Returns a type of transaction that should be produced in current run.
|
||||
fn choose_transaction_type(block_number: T::BlockNumber) -> TransactionType {
|
||||
/// A friendlier name for the error that is going to be returned in case we are in the grace
|
||||
/// period.
|
||||
const RECENTLY_SENT: () = ();
|
||||
|
||||
// Start off by creating a reference to Local Storage value.
|
||||
// Since the local storage is common for all offchain workers, it's a good practice
|
||||
// to prepend your entry with the module name.
|
||||
let val = StorageValueRef::persistent(b"example_ocw::last_send");
|
||||
// The Local Storage is persisted and shared between runs of the offchain workers,
|
||||
// and offchain workers may run concurrently. We can use the `mutate` function, to
|
||||
// write a storage entry in an atomic fashion. Under the hood it uses `compare_and_set`
|
||||
// low-level method of local storage API, which means that only one worker
|
||||
// will be able to "acquire a lock" and send a transaction if multiple workers
|
||||
// happen to be executed concurrently.
|
||||
let res = val.mutate(|last_send: Result<Option<T::BlockNumber>, StorageRetrievalError>| {
|
||||
match last_send {
|
||||
// If we already have a value in storage and the block number is recent enough
|
||||
// we avoid sending another transaction at this time.
|
||||
Ok(Some(block)) if block_number < block + T::GracePeriod::get() =>
|
||||
Err(RECENTLY_SENT),
|
||||
// In every other case we attempt to acquire the lock and send a transaction.
|
||||
_ => Ok(block_number),
|
||||
}
|
||||
});
|
||||
|
||||
// The result of `mutate` call will give us a nested `Result` type.
|
||||
// The first one matches the return of the closure passed to `mutate`, i.e.
|
||||
// if we return `Err` from the closure, we get an `Err` here.
|
||||
// In case we return `Ok`, here we will have another (inner) `Result` that indicates
|
||||
// if the value has been set to the storage correctly - i.e. if it wasn't
|
||||
// written to in the meantime.
|
||||
match res {
|
||||
// The value has been set correctly, which means we can safely send a transaction now.
|
||||
Ok(block_number) => {
|
||||
// Depending if the block is even or odd we will send a `Signed` or `Unsigned`
|
||||
// transaction.
|
||||
// Note that this logic doesn't really guarantee that the transactions will be sent
|
||||
// in an alternating fashion (i.e. fairly distributed). Depending on the execution
|
||||
// order and lock acquisition, we may end up for instance sending two `Signed`
|
||||
// transactions in a row. If a strict order is desired, it's better to use
|
||||
// the storage entry for that. (for instance store both block number and a flag
|
||||
// indicating the type of next transaction to send).
|
||||
let transaction_type = block_number % 3u32.into();
|
||||
if transaction_type == Zero::zero() {
|
||||
TransactionType::Signed
|
||||
} else if transaction_type == T::BlockNumber::from(1u32) {
|
||||
TransactionType::UnsignedForAny
|
||||
} else if transaction_type == T::BlockNumber::from(2u32) {
|
||||
TransactionType::UnsignedForAll
|
||||
} else {
|
||||
TransactionType::Raw
|
||||
}
|
||||
},
|
||||
// We are in the grace period, we should not send a transaction this time.
|
||||
Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => TransactionType::None,
|
||||
// We wanted to send a transaction, but failed to write the block number (acquire a
|
||||
// lock). This indicates that another offchain worker that was running concurrently
|
||||
// most likely executed the same logic and succeeded at writing to storage.
|
||||
// Thus we don't really want to send the transaction, knowing that the other run
|
||||
// already did.
|
||||
Err(MutateStorageError::ConcurrentModification(_)) => TransactionType::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper function to fetch the price and send signed transaction.
|
||||
fn fetch_price_and_send_signed() -> Result<(), &'static str> {
|
||||
let signer = Signer::<T, T::AuthorityId>::all_accounts();
|
||||
if !signer.can_sign() {
|
||||
return Err(
|
||||
"No local accounts available. Consider adding one via `author_insertKey` RPC.",
|
||||
)?
|
||||
}
|
||||
// Make an external HTTP request to fetch the current price.
|
||||
// Note this call will block until response is received.
|
||||
let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?;
|
||||
|
||||
// Using `send_signed_transaction` associated type we create and submit a transaction
|
||||
// representing the call, we've just created.
|
||||
// Submit signed will return a vector of results for all accounts that were found in the
|
||||
// local keystore with expected `KEY_TYPE`.
|
||||
let results = signer.send_signed_transaction(|_account| {
|
||||
// Received price is wrapped into a call to `submit_price` public function of this
|
||||
// pallet. This means that the transaction, when executed, will simply call that
|
||||
// function passing `price` as an argument.
|
||||
Call::submit_price { price }
|
||||
});
|
||||
|
||||
for (acc, res) in &results {
|
||||
match res {
|
||||
Ok(()) => log::info!("[{:?}] Submitted price of {} cents", acc.id, price),
|
||||
Err(e) => log::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A helper function to fetch the price and send a raw unsigned transaction.
|
||||
fn fetch_price_and_send_raw_unsigned(block_number: T::BlockNumber) -> Result<(), &'static str> {
|
||||
// Make sure we don't fetch the price if unsigned transaction is going to be rejected
|
||||
// anyway.
|
||||
let next_unsigned_at = <NextUnsignedAt<T>>::get();
|
||||
if next_unsigned_at > block_number {
|
||||
return Err("Too early to send unsigned transaction")
|
||||
}
|
||||
|
||||
// Make an external HTTP request to fetch the current price.
|
||||
// Note this call will block until response is received.
|
||||
let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?;
|
||||
|
||||
// Received price is wrapped into a call to `submit_price_unsigned` public function of this
|
||||
// pallet. This means that the transaction, when executed, will simply call that function
|
||||
// passing `price` as an argument.
|
||||
let call = Call::submit_price_unsigned { block_number, price };
|
||||
|
||||
// Now let's create a transaction out of this call and submit it to the pool.
|
||||
// Here we showcase two ways to send an unsigned transaction / unsigned payload (raw)
|
||||
//
|
||||
// By default unsigned transactions are disallowed, so we need to whitelist this case
|
||||
// by writing `UnsignedValidator`. Note that it's EXTREMELY important to carefuly
|
||||
// implement unsigned validation logic, as any mistakes can lead to opening DoS or spam
|
||||
// attack vectors. See validation logic docs for more details.
|
||||
//
|
||||
SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into())
|
||||
.map_err(|()| "Unable to submit unsigned transaction.")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A helper function to fetch the price, sign payload and send an unsigned transaction
|
||||
fn fetch_price_and_send_unsigned_for_any_account(
|
||||
block_number: T::BlockNumber,
|
||||
) -> Result<(), &'static str> {
|
||||
// Make sure we don't fetch the price if unsigned transaction is going to be rejected
|
||||
// anyway.
|
||||
let next_unsigned_at = <NextUnsignedAt<T>>::get();
|
||||
if next_unsigned_at > block_number {
|
||||
return Err("Too early to send unsigned transaction")
|
||||
}
|
||||
|
||||
// Make an external HTTP request to fetch the current price.
|
||||
// Note this call will block until response is received.
|
||||
let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?;
|
||||
|
||||
// -- Sign using any account
|
||||
let (_, result) = Signer::<T, T::AuthorityId>::any_account()
|
||||
.send_unsigned_transaction(
|
||||
|account| PricePayload { price, block_number, public: account.public.clone() },
|
||||
|payload, signature| Call::submit_price_unsigned_with_signed_payload {
|
||||
price_payload: payload,
|
||||
signature,
|
||||
},
|
||||
)
|
||||
.ok_or("No local accounts accounts available.")?;
|
||||
result.map_err(|()| "Unable to submit transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A helper function to fetch the price, sign payload and send an unsigned transaction
|
||||
fn fetch_price_and_send_unsigned_for_all_accounts(
|
||||
block_number: T::BlockNumber,
|
||||
) -> Result<(), &'static str> {
|
||||
// Make sure we don't fetch the price if unsigned transaction is going to be rejected
|
||||
// anyway.
|
||||
let next_unsigned_at = <NextUnsignedAt<T>>::get();
|
||||
if next_unsigned_at > block_number {
|
||||
return Err("Too early to send unsigned transaction")
|
||||
}
|
||||
|
||||
// Make an external HTTP request to fetch the current price.
|
||||
// Note this call will block until response is received.
|
||||
let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?;
|
||||
|
||||
// -- Sign using all accounts
|
||||
let transaction_results = Signer::<T, T::AuthorityId>::all_accounts()
|
||||
.send_unsigned_transaction(
|
||||
|account| PricePayload { price, block_number, public: account.public.clone() },
|
||||
|payload, signature| Call::submit_price_unsigned_with_signed_payload {
|
||||
price_payload: payload,
|
||||
signature,
|
||||
},
|
||||
);
|
||||
for (_account_id, result) in transaction_results.into_iter() {
|
||||
if result.is_err() {
|
||||
return Err("Unable to submit transaction")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch current price and return the result in cents.
|
||||
fn fetch_price() -> Result<u32, http::Error> {
|
||||
// We want to keep the offchain worker execution time reasonable, so we set a hard-coded
|
||||
// deadline to 2s to complete the external call.
|
||||
// You can also wait idefinitely for the response, however you may still get a timeout
|
||||
// coming from the host machine.
|
||||
let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000));
|
||||
// Initiate an external HTTP GET request.
|
||||
// This is using high-level wrappers from `sp_runtime`, for the low-level calls that
|
||||
// you can find in `sp_io`. The API is trying to be similar to `reqwest`, but
|
||||
// since we are running in a custom WASM execution environment we can't simply
|
||||
// import the library here.
|
||||
let request =
|
||||
http::Request::get("https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD");
|
||||
// We set the deadline for sending of the request, note that awaiting response can
|
||||
// have a separate deadline. Next we send the request, before that it's also possible
|
||||
// to alter request headers or stream body content in case of non-GET requests.
|
||||
let pending = request.deadline(deadline).send().map_err(|_| http::Error::IoError)?;
|
||||
|
||||
// The request is already being processed by the host, we are free to do anything
|
||||
// else in the worker (we can send multiple concurrent requests too).
|
||||
// At some point however we probably want to check the response though,
|
||||
// so we can block current thread and wait for it to finish.
|
||||
// Note that since the request is being driven by the host, we don't have to wait
|
||||
// for the request to have it complete, we will just not read the response.
|
||||
let response = pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??;
|
||||
// Let's check the status code before we proceed to reading the response.
|
||||
if response.code != 200 {
|
||||
log::warn!("Unexpected status code: {}", response.code);
|
||||
return Err(http::Error::Unknown)
|
||||
}
|
||||
|
||||
// Next we want to fully read the response body and collect it to a vector of bytes.
|
||||
// Note that the return object allows you to read the body in chunks as well
|
||||
// with a way to control the deadline.
|
||||
let body = response.body().collect::<Vec<u8>>();
|
||||
|
||||
// Create a str slice from the body.
|
||||
let body_str = sp_std::str::from_utf8(&body).map_err(|_| {
|
||||
log::warn!("No UTF8 body");
|
||||
http::Error::Unknown
|
||||
})?;
|
||||
|
||||
let price = match Self::parse_price(body_str) {
|
||||
Some(price) => Ok(price),
|
||||
None => {
|
||||
log::warn!("Unable to extract price from the response: {:?}", body_str);
|
||||
Err(http::Error::Unknown)
|
||||
},
|
||||
}?;
|
||||
|
||||
log::warn!("Got price: {} cents", price);
|
||||
|
||||
Ok(price)
|
||||
}
|
||||
|
||||
/// Parse the price from the given JSON string using `lite-json`.
|
||||
///
|
||||
/// Returns `None` when parsing failed or `Some(price in cents)` when parsing is successful.
|
||||
fn parse_price(price_str: &str) -> Option<u32> {
|
||||
let val = lite_json::parse_json(price_str);
|
||||
let price = match val.ok()? {
|
||||
JsonValue::Object(obj) => {
|
||||
let (_, v) = obj.into_iter().find(|(k, _)| k.iter().copied().eq("USD".chars()))?;
|
||||
match v {
|
||||
JsonValue::Number(number) => number,
|
||||
_ => return None,
|
||||
}
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let exp = price.fraction_length.checked_sub(2).unwrap_or(0);
|
||||
Some(price.integer as u32 * 100 + (price.fraction / 10_u64.pow(exp)) as u32)
|
||||
}
|
||||
|
||||
/// Add new price to the list.
|
||||
fn add_price(who: T::AccountId, price: u32) {
|
||||
log::info!("Adding to the average: {}", price);
|
||||
<Prices<T>>::mutate(|prices| {
|
||||
const MAX_LEN: usize = 64;
|
||||
|
||||
if prices.len() < MAX_LEN {
|
||||
prices.push(price);
|
||||
} else {
|
||||
prices[price as usize % MAX_LEN] = price;
|
||||
}
|
||||
});
|
||||
|
||||
let average = Self::average_price()
|
||||
.expect("The average is not empty, because it was just mutated; qed");
|
||||
log::info!("Current average price is: {}", average);
|
||||
// here we are raising the NewPrice event
|
||||
Self::deposit_event(Event::NewPrice(price, who));
|
||||
}
|
||||
|
||||
/// Calculate current average price.
|
||||
fn average_price() -> Option<u32> {
|
||||
let prices = <Prices<T>>::get();
|
||||
if prices.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(prices.iter().fold(0_u32, |a, b| a.saturating_add(*b)) / prices.len() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_transaction_parameters(
|
||||
block_number: &T::BlockNumber,
|
||||
new_price: &u32,
|
||||
) -> TransactionValidity {
|
||||
// Now let's check if the transaction has any chance to succeed.
|
||||
let next_unsigned_at = <NextUnsignedAt<T>>::get();
|
||||
if &next_unsigned_at > block_number {
|
||||
return InvalidTransaction::Stale.into()
|
||||
}
|
||||
// Let's make sure to reject transactions from the future.
|
||||
let current_block = <system::Pallet<T>>::block_number();
|
||||
if ¤t_block < block_number {
|
||||
return InvalidTransaction::Future.into()
|
||||
}
|
||||
|
||||
// We prioritize transactions that are more far away from current average.
|
||||
//
|
||||
// Note this doesn't make much sense when building an actual oracle, but this example
|
||||
// is here mostly to show off offchain workers capabilities, not about building an
|
||||
// oracle.
|
||||
let avg_price = Self::average_price()
|
||||
.map(|price| if &price > new_price { price - new_price } else { new_price - price })
|
||||
.unwrap_or(0);
|
||||
|
||||
ValidTransaction::with_tag_prefix("ExampleOffchainWorker")
|
||||
// We set base priority to 2**20 and hope it's included before any other
|
||||
// transactions in the pool. Next we tweak the priority depending on how much
|
||||
// it differs from the current average. (the more it differs the more priority it
|
||||
// has).
|
||||
.priority(T::UnsignedPriority::get().saturating_add(avg_price as _))
|
||||
// This transaction does not require anything else to go before into the pool.
|
||||
// In theory we could require `previous_unsigned_at` transaction to go first,
|
||||
// but it's not necessary in our case.
|
||||
//.and_requires()
|
||||
// We set the `provides` tag to be the same as `next_unsigned_at`. This makes
|
||||
// sure only one transaction produced after `next_unsigned_at` will ever
|
||||
// get to the transaction pool and will end up in the block.
|
||||
// We can still have multiple transactions compete for the same "spot",
|
||||
// and the one with higher priority will replace other one in the pool.
|
||||
.and_provides(next_unsigned_at)
|
||||
// The transaction is only valid for next 5 blocks. After that it's
|
||||
// going to be revalidated by the pool.
|
||||
.longevity(5)
|
||||
// It's fine to propagate that transaction to other peers, which means it can be
|
||||
// created even by nodes that don't produce blocks.
|
||||
// Note that sometimes it's better to keep it for yourself (if you are the block
|
||||
// producer), since for instance in some schemes others may copy your solution and
|
||||
// claim a reward.
|
||||
.propagate(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate as example_offchain_worker;
|
||||
use crate::*;
|
||||
use codec::Decode;
|
||||
use frame_support::{assert_ok, parameter_types};
|
||||
use sp_core::{
|
||||
offchain::{testing, OffchainWorkerExt, TransactionPoolExt},
|
||||
sr25519::Signature,
|
||||
H256,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use sp_keystore::{testing::KeyStore, KeystoreExt, SyncCryptoStore};
|
||||
use sp_runtime::{
|
||||
testing::{Header, TestXt},
|
||||
traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify},
|
||||
RuntimeAppPublic,
|
||||
};
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
// For testing the module, we construct a mock runtime.
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Example: example_offchain_worker::{Pallet, Call, Storage, Event<T>, ValidateUnsigned},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(1024);
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type Origin = Origin;
|
||||
type Call = Call;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = sp_core::sr25519::Public;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
type Extrinsic = TestXt<Call, ()>;
|
||||
type AccountId = <<Signature as Verify>::Signer as IdentifyAccount>::AccountId;
|
||||
|
||||
impl frame_system::offchain::SigningTypes for Test {
|
||||
type Public = <Signature as Verify>::Signer;
|
||||
type Signature = Signature;
|
||||
}
|
||||
|
||||
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Test
|
||||
where
|
||||
Call: From<LocalCall>,
|
||||
{
|
||||
type OverarchingCall = Call;
|
||||
type Extrinsic = Extrinsic;
|
||||
}
|
||||
|
||||
impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for Test
|
||||
where
|
||||
Call: From<LocalCall>,
|
||||
{
|
||||
fn create_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>(
|
||||
call: Call,
|
||||
_public: <Signature as Verify>::Signer,
|
||||
_account: AccountId,
|
||||
nonce: u64,
|
||||
) -> Option<(Call, <Extrinsic as ExtrinsicT>::SignaturePayload)> {
|
||||
Some((call, (nonce, ())))
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const GracePeriod: u64 = 5;
|
||||
pub const UnsignedInterval: u64 = 128;
|
||||
pub const UnsignedPriority: u64 = 1 << 20;
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Event = Event;
|
||||
type AuthorityId = crypto::TestAuthId;
|
||||
type Call = Call;
|
||||
type GracePeriod = GracePeriod;
|
||||
type UnsignedInterval = UnsignedInterval;
|
||||
type UnsignedPriority = UnsignedPriority;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_aggregates_the_price() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
assert_eq!(Example::average_price(), None);
|
||||
|
||||
assert_ok!(Example::submit_price(Origin::signed(Default::default()), 27));
|
||||
assert_eq!(Example::average_price(), Some(27));
|
||||
|
||||
assert_ok!(Example::submit_price(Origin::signed(Default::default()), 43));
|
||||
assert_eq!(Example::average_price(), Some(35));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_http_call_and_parse_result() {
|
||||
let (offchain, state) = testing::TestOffchainExt::new();
|
||||
let mut t = sp_io::TestExternalities::default();
|
||||
t.register_extension(OffchainWorkerExt::new(offchain));
|
||||
|
||||
price_oracle_response(&mut state.write());
|
||||
|
||||
t.execute_with(|| {
|
||||
// when
|
||||
let price = Example::fetch_price().unwrap();
|
||||
// then
|
||||
assert_eq!(price, 15523);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knows_how_to_mock_several_http_calls() {
|
||||
let (offchain, state) = testing::TestOffchainExt::new();
|
||||
let mut t = sp_io::TestExternalities::default();
|
||||
t.register_extension(OffchainWorkerExt::new(offchain));
|
||||
|
||||
{
|
||||
let mut state = state.write();
|
||||
state.expect_request(testing::PendingRequest {
|
||||
method: "GET".into(),
|
||||
uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(),
|
||||
response: Some(br#"{"USD": 1}"#.to_vec()),
|
||||
sent: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
state.expect_request(testing::PendingRequest {
|
||||
method: "GET".into(),
|
||||
uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(),
|
||||
response: Some(br#"{"USD": 2}"#.to_vec()),
|
||||
sent: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
state.expect_request(testing::PendingRequest {
|
||||
method: "GET".into(),
|
||||
uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(),
|
||||
response: Some(br#"{"USD": 3}"#.to_vec()),
|
||||
sent: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
t.execute_with(|| {
|
||||
let price1 = Example::fetch_price().unwrap();
|
||||
let price2 = Example::fetch_price().unwrap();
|
||||
let price3 = Example::fetch_price().unwrap();
|
||||
|
||||
assert_eq!(price1, 100);
|
||||
assert_eq!(price2, 200);
|
||||
assert_eq!(price3, 300);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_submit_signed_transaction_on_chain() {
|
||||
const PHRASE: &str =
|
||||
"news slush supreme milk chapter athlete soap sausage put clutch what kitten";
|
||||
|
||||
let (offchain, offchain_state) = testing::TestOffchainExt::new();
|
||||
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
|
||||
let keystore = KeyStore::new();
|
||||
SyncCryptoStore::sr25519_generate_new(
|
||||
&keystore,
|
||||
crate::crypto::Public::ID,
|
||||
Some(&format!("{}/hunter1", PHRASE)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut t = sp_io::TestExternalities::default();
|
||||
t.register_extension(OffchainWorkerExt::new(offchain));
|
||||
t.register_extension(TransactionPoolExt::new(pool));
|
||||
t.register_extension(KeystoreExt(Arc::new(keystore)));
|
||||
|
||||
price_oracle_response(&mut offchain_state.write());
|
||||
|
||||
t.execute_with(|| {
|
||||
// when
|
||||
Example::fetch_price_and_send_signed().unwrap();
|
||||
// then
|
||||
let tx = pool_state.write().transactions.pop().unwrap();
|
||||
assert!(pool_state.read().transactions.is_empty());
|
||||
let tx = Extrinsic::decode(&mut &*tx).unwrap();
|
||||
assert_eq!(tx.signature.unwrap().0, 0);
|
||||
assert_eq!(tx.call, Call::Example(crate::Call::submit_price { price: 15523 }));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_submit_unsigned_transaction_on_chain_for_any_account() {
|
||||
const PHRASE: &str =
|
||||
"news slush supreme milk chapter athlete soap sausage put clutch what kitten";
|
||||
let (offchain, offchain_state) = testing::TestOffchainExt::new();
|
||||
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
|
||||
|
||||
let keystore = KeyStore::new();
|
||||
|
||||
SyncCryptoStore::sr25519_generate_new(
|
||||
&keystore,
|
||||
crate::crypto::Public::ID,
|
||||
Some(&format!("{}/hunter1", PHRASE)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let public_key = SyncCryptoStore::sr25519_public_keys(&keystore, crate::crypto::Public::ID)
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let mut t = sp_io::TestExternalities::default();
|
||||
t.register_extension(OffchainWorkerExt::new(offchain));
|
||||
t.register_extension(TransactionPoolExt::new(pool));
|
||||
t.register_extension(KeystoreExt(Arc::new(keystore)));
|
||||
|
||||
price_oracle_response(&mut offchain_state.write());
|
||||
|
||||
let price_payload = PricePayload {
|
||||
block_number: 1,
|
||||
price: 15523,
|
||||
public: <Test as SigningTypes>::Public::from(public_key),
|
||||
};
|
||||
|
||||
// let signature = price_payload.sign::<crypto::TestAuthId>().unwrap();
|
||||
t.execute_with(|| {
|
||||
// when
|
||||
Example::fetch_price_and_send_unsigned_for_any_account(1).unwrap();
|
||||
// then
|
||||
let tx = pool_state.write().transactions.pop().unwrap();
|
||||
let tx = Extrinsic::decode(&mut &*tx).unwrap();
|
||||
assert_eq!(tx.signature, None);
|
||||
if let Call::Example(crate::Call::submit_price_unsigned_with_signed_payload {
|
||||
price_payload: body,
|
||||
signature,
|
||||
}) = tx.call
|
||||
{
|
||||
assert_eq!(body, price_payload);
|
||||
|
||||
let signature_valid =
|
||||
<PricePayload<
|
||||
<Test as SigningTypes>::Public,
|
||||
<Test as frame_system::Config>::BlockNumber,
|
||||
> as SignedPayload<Test>>::verify::<crypto::TestAuthId>(&price_payload, signature);
|
||||
|
||||
assert!(signature_valid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_submit_unsigned_transaction_on_chain_for_all_accounts() {
|
||||
const PHRASE: &str =
|
||||
"news slush supreme milk chapter athlete soap sausage put clutch what kitten";
|
||||
let (offchain, offchain_state) = testing::TestOffchainExt::new();
|
||||
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
|
||||
|
||||
let keystore = KeyStore::new();
|
||||
|
||||
SyncCryptoStore::sr25519_generate_new(
|
||||
&keystore,
|
||||
crate::crypto::Public::ID,
|
||||
Some(&format!("{}/hunter1", PHRASE)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let public_key = SyncCryptoStore::sr25519_public_keys(&keystore, crate::crypto::Public::ID)
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let mut t = sp_io::TestExternalities::default();
|
||||
t.register_extension(OffchainWorkerExt::new(offchain));
|
||||
t.register_extension(TransactionPoolExt::new(pool));
|
||||
t.register_extension(KeystoreExt(Arc::new(keystore)));
|
||||
|
||||
price_oracle_response(&mut offchain_state.write());
|
||||
|
||||
let price_payload = PricePayload {
|
||||
block_number: 1,
|
||||
price: 15523,
|
||||
public: <Test as SigningTypes>::Public::from(public_key),
|
||||
};
|
||||
|
||||
// let signature = price_payload.sign::<crypto::TestAuthId>().unwrap();
|
||||
t.execute_with(|| {
|
||||
// when
|
||||
Example::fetch_price_and_send_unsigned_for_all_accounts(1).unwrap();
|
||||
// then
|
||||
let tx = pool_state.write().transactions.pop().unwrap();
|
||||
let tx = Extrinsic::decode(&mut &*tx).unwrap();
|
||||
assert_eq!(tx.signature, None);
|
||||
if let Call::Example(crate::Call::submit_price_unsigned_with_signed_payload {
|
||||
price_payload: body,
|
||||
signature,
|
||||
}) = tx.call
|
||||
{
|
||||
assert_eq!(body, price_payload);
|
||||
|
||||
let signature_valid =
|
||||
<PricePayload<
|
||||
<Test as SigningTypes>::Public,
|
||||
<Test as frame_system::Config>::BlockNumber,
|
||||
> as SignedPayload<Test>>::verify::<crypto::TestAuthId>(&price_payload, signature);
|
||||
|
||||
assert!(signature_valid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_submit_raw_unsigned_transaction_on_chain() {
|
||||
let (offchain, offchain_state) = testing::TestOffchainExt::new();
|
||||
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
|
||||
|
||||
let keystore = KeyStore::new();
|
||||
|
||||
let mut t = sp_io::TestExternalities::default();
|
||||
t.register_extension(OffchainWorkerExt::new(offchain));
|
||||
t.register_extension(TransactionPoolExt::new(pool));
|
||||
t.register_extension(KeystoreExt(Arc::new(keystore)));
|
||||
|
||||
price_oracle_response(&mut offchain_state.write());
|
||||
|
||||
t.execute_with(|| {
|
||||
// when
|
||||
Example::fetch_price_and_send_raw_unsigned(1).unwrap();
|
||||
// then
|
||||
let tx = pool_state.write().transactions.pop().unwrap();
|
||||
assert!(pool_state.read().transactions.is_empty());
|
||||
let tx = Extrinsic::decode(&mut &*tx).unwrap();
|
||||
assert_eq!(tx.signature, None);
|
||||
assert_eq!(
|
||||
tx.call,
|
||||
Call::Example(crate::Call::submit_price_unsigned { block_number: 1, price: 15523 })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn price_oracle_response(state: &mut testing::OffchainState) {
|
||||
state.expect_request(testing::PendingRequest {
|
||||
method: "GET".into(),
|
||||
uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(),
|
||||
response: Some(br#"{"USD": 155.23}"#.to_vec()),
|
||||
sent: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_price_works() {
|
||||
let test_data = vec![
|
||||
("{\"USD\":6536.92}", Some(653692)),
|
||||
("{\"USD\":65.92}", Some(6592)),
|
||||
("{\"USD\":6536.924565}", Some(653692)),
|
||||
("{\"USD\":6536}", Some(653600)),
|
||||
("{\"USD2\":6536}", None),
|
||||
("{\"USD\":\"6432\"}", None),
|
||||
];
|
||||
|
||||
for (json, expected) in test_data {
|
||||
assert_eq!(expected, Example::parse_price(json));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user