diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4710b9c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,103 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'cyferio-hub-node'", + "cargo": { + "args": [ + "build", + "--bin=cyferio-hub-node", + "--package=cyferio-hub-node" + ], + "filter": { + "name": "cyferio-hub-node", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}", + "postDebugTask": "cargo run --bin cyferio-hub-node -- --dev" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'cyferio-hub-node'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=cyferio-hub-node", + "--package=cyferio-hub-node" + ], + "filter": { + "name": "cyferio-hub-node", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'cyferio_hub_runtime'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=cyferio-hub-runtime" + ], + "filter": { + "name": "cyferio_hub_runtime", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'pallet_offchain_worker'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=pallet-offchain-worker" + ], + "filter": { + "name": "pallet_offchain_worker", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'pallet_template'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=pallet-template" + ], + "filter": { + "name": "pallet_template", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/Cargo.lock b/Cargo.lock index 242e336..6ccdba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ dependencies = [ "cipher 0.4.4", "ctr", "ghash", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -1342,7 +1342,7 @@ checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array 0.14.7", "rand_core", - "subtle 2.6.1", + "subtle 2.4.1", "zeroize", ] @@ -1374,7 +1374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ "generic-array 0.14.7", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -1398,7 +1398,7 @@ dependencies = [ "digest 0.10.7", "fiat-crypto", "rustc_version", - "subtle 2.6.1", + "subtle 2.4.1", "zeroize", ] @@ -1510,9 +1510,11 @@ dependencies = [ "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", + "log", "pallet-aura", "pallet-balances", "pallet-grandpa", + "pallet-offchain-worker", "pallet-sudo", "pallet-template", "pallet-timestamp", @@ -1530,6 +1532,7 @@ dependencies = [ "sp-offchain", "sp-runtime", "sp-session", + "sp-std", "sp-storage", "sp-transaction-pool", "sp-version", @@ -1690,7 +1693,7 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -1848,7 +1851,7 @@ dependencies = [ "rand_core", "serde", "sha2 0.10.8", - "subtle 2.6.1", + "subtle 2.4.1", "zeroize", ] @@ -1889,7 +1892,7 @@ dependencies = [ "rand_core", "sec1", "serdect", - "subtle 2.6.1", + "subtle 2.4.1", "zeroize", ] @@ -2044,7 +2047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -2717,7 +2720,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", "rand_core", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -4049,7 +4052,7 @@ checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" dependencies = [ "crunchy", "digest 0.9.0", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -4138,6 +4141,24 @@ dependencies = [ "keystream", ] +[[package]] +name = "lite-json" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0e787ffe1153141a0f6f6d759fdf1cc34b1226e088444523812fd412a5cca2" +dependencies = [ + "lite-parser", +] + +[[package]] +name = "lite-parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d5f9dc37c52d889a21fd701983d02bb6a84f852c5140a6c80ef4557f7dc29e" +dependencies = [ + "paste", +] + [[package]] name = "litep2p" version = "0.6.2" @@ -4454,7 +4475,7 @@ dependencies = [ "rand", "rand_chacha", "rand_distr", - "subtle 2.6.1", + "subtle 2.4.1", "thiserror", "zeroize", ] @@ -5097,6 +5118,25 @@ dependencies = [ "sp-staking", ] +[[package]] +name = "pallet-offchain-worker" +version = "28.0.0" +dependencies = [ + "frame-support", + "frame-system", + "lite-json", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-session" version = "37.0.0" @@ -5350,7 +5390,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -6330,7 +6370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac 0.12.1", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -7646,7 +7686,7 @@ dependencies = [ "rand_core", "serde_bytes", "sha2 0.10.8", - "subtle 2.6.1", + "subtle 2.4.1", "zeroize", ] @@ -7698,7 +7738,7 @@ dependencies = [ "generic-array 0.14.7", "pkcs8", "serdect", - "subtle 2.6.1", + "subtle 2.4.1", "zeroize", ] @@ -8017,7 +8057,7 @@ dependencies = [ "ring 0.17.8", "rustc_version", "sha2 0.10.8", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] @@ -8985,9 +9025,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" [[package]] name = "subtle" -version = "2.6.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" @@ -9709,7 +9749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", - "subtle 2.6.1", + "subtle 2.4.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 670de6f..689da86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [workspace] members = [ "node", + "pallets/offchain-worker", "pallets/template", "runtime", ] @@ -16,6 +17,7 @@ resolver = "2" [workspace.dependencies] cyferio-hub-runtime = { path = "./runtime", default-features = false } pallet-template = { path = "./pallets/template", default-features = false } +pallet-offchain-worker = { path = "./pallets/offchain-worker", default-features = false } clap = { version = "4.5.3" } frame-benchmarking-cli = { version = "42.0.0", default-features = false } frame-system = { version = "37.0.0", default-features = false } @@ -68,7 +70,11 @@ scale-info = { version = "2.11.1", default-features = false } sp-genesis-builder = { version = "0.15.0", default-features = false } sp-offchain = { version = "34.0.0", default-features = false } sp-session = { version = "35.0.0", default-features = false } +sp-keystore = { version = "0.40.0", default-features = false } sp-storage = { version = "21.0.0", default-features = false } +sp-std = { version = "14.0.0", default-features = false } sp-transaction-pool = { version = "34.0.0", default-features = false } sp-version = { version = "37.0.0", default-features = false } substrate-wasm-builder = { version = "24.0.0", default-features = false } +lite-json = { version = "0.2.0", default-features = false } +log = { version = "0.4.22", default-features = false } diff --git a/cyferio-config.toml b/cyferio-config.toml new file mode 100644 index 0000000..e7794b1 --- /dev/null +++ b/cyferio-config.toml @@ -0,0 +1,2 @@ +[cyferio] +keystore_path = "/Users/feng/Desktop/obelisk/cyferio/cyferio-hub-node/cyferio-keystore.json" diff --git a/cyferio-keystore.json b/cyferio-keystore.json new file mode 100644 index 0000000..1137e8d --- /dev/null +++ b/cyferio-keystore.json @@ -0,0 +1 @@ +testkeystorecyferiodatai==== diff --git a/keystore b/keystore new file mode 100644 index 0000000..e69de29 diff --git a/node/src/cli.rs b/node/src/cli.rs index b2c53aa..49f7325 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -1,4 +1,5 @@ use sc_cli::RunCmd; +use crate::cyferio_cli::set_cyferio_keystore::SetKeystoreCmd; #[derive(Debug, clap::Parser)] pub struct Cli { @@ -43,4 +44,7 @@ pub enum Subcommand { /// Db meta columns information. ChainInfo(sc_cli::ChainInfoCmd), + + /// Set the keystore + SetKeystore(SetKeystoreCmd), } diff --git a/node/src/command.rs b/node/src/command.rs index b7e44aa..27ac530 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -9,6 +9,10 @@ use sc_cli::SubstrateCli; use sc_service::PartialComponents; use cyferio_hub_runtime::{Block, EXISTENTIAL_DEPOSIT}; use sp_keyring::Sr25519Keyring; +use sp_core::offchain; +use sp_core::offchain::StorageKind; +use sp_io::offchain::local_storage_set; + impl SubstrateCli for Cli { fn impl_name() -> String { @@ -174,6 +178,22 @@ pub fn run() -> sc_cli::Result<()> { let runner = cli.create_runner(cmd)?; runner.sync_run(|config| cmd.run::(&config)) }, + Some(Subcommand::SetKeystore(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config: sc_service::Configuration| { + let content = std::fs::read(&cmd.keystore_path) + .map_err(|e| format!("Error reading keystore file: {}", e))?; + + sp_io::offchain::local_storage_set( + sp_core::offchain::StorageKind::PERSISTENT, + b"cyferio-keystore", + &content + ); + + println!("Keystore set successfully"); + Ok(()) + }) + }, None => { let runner = cli.create_runner(&cli.run)?; runner.run_node_until_exit(|config| async move { diff --git a/node/src/cyferio_cli/mod.rs b/node/src/cyferio_cli/mod.rs new file mode 100644 index 0000000..cfa7088 --- /dev/null +++ b/node/src/cyferio_cli/mod.rs @@ -0,0 +1 @@ +pub mod set_cyferio_keystore; \ No newline at end of file diff --git a/node/src/cyferio_cli/set_cyferio_keystore.rs b/node/src/cyferio_cli/set_cyferio_keystore.rs new file mode 100644 index 0000000..2e344e2 --- /dev/null +++ b/node/src/cyferio_cli/set_cyferio_keystore.rs @@ -0,0 +1,18 @@ +use sc_cli::CliConfiguration; +use sc_cli::SharedParams; + +#[derive(Debug, clap::Parser)] +pub struct SetKeystoreCmd { + #[arg(short, long)] + pub keystore_path: std::path::PathBuf, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, +} + +impl CliConfiguration for SetKeystoreCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } +} diff --git a/node/src/main.rs b/node/src/main.rs index 8918dd4..bf3bd79 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -7,6 +7,7 @@ mod cli; mod command; mod rpc; mod service; +mod cyferio_cli; fn main() -> sc_cli::Result<()> { command::run() diff --git a/pallets/offchain-worker/Cargo.toml b/pallets/offchain-worker/Cargo.toml new file mode 100644 index 0000000..1df8cb8 --- /dev/null +++ b/pallets/offchain-worker/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pallet-offchain-worker" +version = "28.0.0" +authors.workspace = true +edition.workspace = true +license = "MIT-0" +homepage.workspace = true +repository.workspace = true +description = "FRAME example pallet for offchain worker" +readme = "README.md" +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = '1.0.188', default-features = false, features = ['derive'] } +serde_json = { version = '1.0.108', default-features = false, features = ['alloc'] } +codec = { workspace = true } +lite-json = { workspace = true } +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +frame-support.workspace = true +frame-system.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-keystore = { optional = true, workspace = true } +sp-runtime.workspace = true +sp-std.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "lite-json/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-keystore/std", + "sp-runtime/std", + "sp-std/std", +] + +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/offchain-worker/src/lib.rs b/pallets/offchain-worker/src/lib.rs new file mode 100644 index 0000000..f398eb4 --- /dev/null +++ b/pallets/offchain-worker/src/lib.rs @@ -0,0 +1,821 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! +//! # 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)] + +extern crate alloc; + +use codec::{Decode, Encode}; +use frame_support::traits::Get; +use frame_system::{ + self as system, + offchain::{ + AppCrypto, CreateSignedTransaction, SendSignedTransaction, SendUnsignedTransaction, + SignedPayload, Signer, SigningTypes, SubmitTransaction, + }, + pallet_prelude::BlockNumberFor, +}; +use lite_json::json::JsonValue; +use sp_core::crypto::KeyTypeId; +use sp_runtime::{ + offchain::{ + http, + storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, + Duration, + StorageKind, + }, + traits::Zero, + transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, + RuntimeDebug, +}; +use sp_io::offchain; + +use sp_std::vec::Vec; +use sp_std::vec; + +use serde::Deserialize; +use serde_json::Value; +use scale_info::prelude::string::String; + +#[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 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<::Signer, Sr25519Signature> + for TestAuthId + { + type RuntimeAppPublic = Public; + type GenericSignature = sp_core::sr25519::Signature; + type GenericPublic = sp_core::sr25519::Public; + } +} + +pub use pallet::*; + +#[derive(Deserialize, Debug)] +struct ResponseData { + epoch: EpochData, +} + +#[derive(Deserialize, Debug)] +struct EpochData { + referenceGasPrice: String, +} + + +#[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> + frame_system::Config { + /// The identifier type for an offchain worker. + type AuthorityId: AppCrypto; + + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + // 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>; + + /// 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>; + + /// 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; + + /// Maximum number of prices. + #[pallet::constant] + type MaxPrices: Get; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet { + /// 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 + /// successfully 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: BlockNumberFor) { + // 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!"); + + // // 尝试从 offchain storage 读取 keystore + // let _keystore_content = match offchain::local_storage_get(StorageKind::PERSISTENT, b"cyferio-keystore") { + // Some(content) => content, + // None => { + // log::error!("Cyferio keystore not found in offchain storage"); + // } + // }; + + // // 使用 keystore_content 进行后续操作 + // log::info!("Cyferio keystore loaded successfully"); + + // 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 = >::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 = Self::average_price(); + log::debug!("Current price: {:?}", average); + + log::info!("===== test_sui start ====="); + let gas_price = Self::test_sui(); + log::info!("gas_price: {:?}", gas_price); + log::info!("===== test_sui end ====="); + + // 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 Pallet { + /// 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 + /// excessive 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::call_index(0)] + #[pallet::weight({0})] + pub fn submit_price(origin: OriginFor, price: u32) -> DispatchResultWithPostInfo { + // Retrieve sender of the transaction. + let who = ensure_signed(origin)?; + // Add the price to the on-chain list. + Self::add_price(Some(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::call_index(1)] + #[pallet::weight({0})] + pub fn submit_price_unsigned( + origin: OriginFor, + _block_number: BlockNumberFor, + 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(None, price); + // now increment the block number at which we expect next unsigned transaction. + let current_block = >::block_number(); + >::put(current_block + T::UnsignedInterval::get()); + Ok(().into()) + } + + #[pallet::call_index(2)] + #[pallet::weight({0})] + pub fn submit_price_unsigned_with_signed_payload( + origin: OriginFor, + price_payload: PricePayload>, + _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(None, price_payload.price); + // now increment the block number at which we expect next unsigned transaction. + let current_block = >::block_number(); + >::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 { + /// Event generated when new price is accepted to contribute to the average. + NewPrice { price: u32, maybe_who: Option }, + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + /// 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::::verify::(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] + pub(super) type Prices = StorageValue<_, BoundedVec, ValueQuery>; + + /// Defines the block when next unsigned transaction will be accepted. + /// + /// To prevent spam of unsigned (and unpaid!) 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] + pub(super) type NextUnsignedAt = StorageValue<_, BlockNumberFor, 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 { + block_number: BlockNumber, + price: u32, + public: Public, +} + +impl SignedPayload for PricePayload> { + fn public(&self) -> T::Public { + self.public.clone() + } +} + +enum TransactionType { + Signed, + UnsignedForAny, + UnsignedForAll, + Raw, + None, +} + +impl Pallet { + /// 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: BlockNumberFor) -> 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"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>, 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) => { + // We will send different transactions based on a random number. + // 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 % 4u32.into(); + if transaction_type == Zero::zero() { + TransactionType::Signed + } else if transaction_type == BlockNumberFor::::from(1u32) { + TransactionType::UnsignedForAny + } else if transaction_type == BlockNumberFor::::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::::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: BlockNumberFor, + ) -> 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::::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 carefully + // implement unsigned validation logic, as any mistakes can lead to opening DoS or spam + // attack vectors. See validation logic docs for more details. + // + SubmitTransaction::>::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: BlockNumberFor, + ) -> 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::::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::::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: BlockNumberFor, + ) -> 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::::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::::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(()) + } + + + + fn test_sui() -> Result { + let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000)); // 增加到 10 秒 + // let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(5_000)); // 增加到 5 秒 + let url = "https://graphql-beta.mainnet.sui.io"; + let request_body = r#"{"query": "query { epoch { referenceGasPrice } }"}"#; + log::info!("Preparing to send request to {}", url); + let request = http::Request::post(url, vec![request_body.clone()]) + .add_header("Content-Type", "application/json"); + let pending = request + .deadline(deadline) + .send() + .map_err(|e| { + log::debug!("发送请求失败: {:?}", e); + http::Error::IoError + })?; + log::info!("Request sent, waiting for response"); + let response = pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??; + log::info!("Response received with status code: {}", response.code); + if response.code != 200 { + log::debug!("Unexpected status code: {}", response.code); + return Err(http::Error::Unknown) + } + + let body = response.body().collect::>(); + log::info!("5"); + let body_str = sp_std::str::from_utf8(&body).map_err(|_| { + log::warn!("No UTF8 body"); + http::Error::Unknown + })?; + let response_json: Value = serde_json::from_str(body_str).map_err(|err| { + log::warn!("Error parsing response body as JSON 1 : {}", err); + http::Error::Unknown + })?; + let response_data: ResponseData = serde_json::from_value(response_json["data"].clone()).map_err(|err| { + log::warn!("Error parsing response body as JSON 2: {}", err); + http::Error::Unknown + })?; + log::info!("Info from Sui response_data: {:?}",response_data); + log::info!("8"); + let reference_gas_price = response_data.epoch.referenceGasPrice; + log::info!("Info from Sui reference_gas_price: {:?}",reference_gas_price); + log::info!("9"); + // let reference_gas_price:&str = match response_data.epoch.referenceGasPrice { + // Some(reference_gas_price) => Ok(reference_gas_price), + // None => { + // log::warn!("Unable to extract price from the response: {:?}", body_str); + // Err(http::Error::Unknown) + // }, + // }?; + + log::info!("Info from Sui: {:?}",reference_gas_price); + log::info!("Current Gas price: {:?}",reference_gas_price); + Ok(reference_gas_price) + // Ok(()) + } + + /// Fetch current price and return the result in cents. + fn fetch_price() -> Result { + // 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 indefinitely 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 `request`, 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::>(); + + // Create a str slice from the body. + let body_str = alloc::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 { + 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.saturating_sub(2); + Some(price.integer as u32 * 100 + (price.fraction / 10_u64.pow(exp)) as u32) + } + + /// Add new price to the list. + fn add_price(maybe_who: Option, price: u32) { + log::info!("Adding to the average: {}", price); + >::mutate(|prices| { + if prices.try_push(price).is_err() { + prices[(price % T::MaxPrices::get()) as usize] = 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, maybe_who }); + } + + /// Calculate current average price. + fn average_price() -> Option { + let prices = Prices::::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: &BlockNumberFor, + new_price: &u32, + ) -> TransactionValidity { + // Now let's check if the transaction has any chance to succeed. + let next_unsigned_at = NextUnsignedAt::::get(); + if &next_unsigned_at > block_number { + return InvalidTransaction::Stale.into() + } + // Let's make sure to reject transactions from the future. + let current_block = >::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("OffchainWorker") + // 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() + } +} \ No newline at end of file diff --git a/pallets/offchain-worker/src/tests.rs b/pallets/offchain-worker/src/tests.rs new file mode 100644 index 0000000..0dcc942 --- /dev/null +++ b/pallets/offchain-worker/src/tests.rs @@ -0,0 +1,390 @@ +// This file is part of Substrate. + +// Copyright (C) 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, derive_impl, parameter_types, + traits::{ConstU32, ConstU64}, +}; +use sp_core::{ + offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, + sr25519::Signature, + H256, +}; + +use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; +use sp_runtime::{ + testing::TestXt, + traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, + RuntimeAppPublic, +}; + +type Block = frame_system::mocking::MockBlock; + +// For testing the module, we construct a mock runtime. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Example: example_offchain_worker, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = sp_core::sr25519::Public; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +type Extrinsic = TestXt; +type AccountId = <::Signer as IdentifyAccount>::AccountId; + +impl frame_system::offchain::SigningTypes for Test { + type Public = ::Signer; + type Signature = Signature; +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +impl frame_system::offchain::CreateSignedTransaction for Test +where + RuntimeCall: From, +{ + fn create_transaction>( + call: RuntimeCall, + _public: ::Signer, + _account: AccountId, + nonce: u64, + ) -> Option<(RuntimeCall, ::SignaturePayload)> { + Some((call, (nonce, ()))) + } +} + +parameter_types! { + pub const UnsignedPriority: u64 = 1 << 20; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type AuthorityId = crypto::TestAuthId; + type GracePeriod = ConstU64<5>; + type UnsignedInterval = ConstU64<128>; + type UnsignedPriority = UnsignedPriority; + type MaxPrices = ConstU32<64>; +} + +fn test_pub() -> sp_core::sr25519::Public { + sp_core::sr25519::Public::from_raw([1u8; 32]) +} + +#[test] +fn it_aggregates_the_price() { + sp_io::TestExternalities::default().execute_with(|| { + assert_eq!(Example::average_price(), None); + + assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 27)); + assert_eq!(Example::average_price(), Some(27)); + + assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 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 = MemoryKeystore::new(); + keystore + .sr25519_generate_new(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::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, RuntimeCall::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 = MemoryKeystore::new(); + + keystore + .sr25519_generate_new(crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE))) + .unwrap(); + + let public_key = *keystore.sr25519_public_keys(crate::crypto::Public::ID).get(0).unwrap(); + + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + t.register_extension(TransactionPoolExt::new(pool)); + t.register_extension(KeystoreExt::new(keystore)); + + price_oracle_response(&mut offchain_state.write()); + + let price_payload = PricePayload { + block_number: 1, + price: 15523, + public: ::Public::from(public_key), + }; + + // let signature = price_payload.sign::().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 RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { + price_payload: body, + signature, + }) = tx.call + { + assert_eq!(body, price_payload); + + let signature_valid = + ::Public, + frame_system::pallet_prelude::BlockNumberFor, + > as SignedPayload>::verify::(&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 = MemoryKeystore::new(); + + keystore + .sr25519_generate_new(crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE))) + .unwrap(); + + let public_key = *keystore.sr25519_public_keys(crate::crypto::Public::ID).get(0).unwrap(); + + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + t.register_extension(TransactionPoolExt::new(pool)); + t.register_extension(KeystoreExt::new(keystore)); + + price_oracle_response(&mut offchain_state.write()); + + let price_payload = PricePayload { + block_number: 1, + price: 15523, + public: ::Public::from(public_key), + }; + + // let signature = price_payload.sign::().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 RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { + price_payload: body, + signature, + }) = tx.call + { + assert_eq!(body, price_payload); + + let signature_valid = + ::Public, + frame_system::pallet_prelude::BlockNumberFor, + > as SignedPayload>::verify::(&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 = MemoryKeystore::new(); + + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + t.register_extension(TransactionPoolExt::new(pool)); + t.register_extension(KeystoreExt::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, + RuntimeCall::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 = alloc::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)); + } +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b7e9a7d..fd3dd36 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -13,6 +13,7 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] +log = { workspace = true } codec = { features = [ "derive", ], workspace = true } @@ -43,11 +44,13 @@ sp-storage.workspace = true sp-transaction-pool.workspace = true sp-version = { features = ["serde"], workspace = true } sp-genesis-builder.workspace = true +sp-std.workspace = true frame-system-rpc-runtime-api.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true frame-benchmarking = { optional = true, workspace = true } frame-system-benchmarking = { optional = true, workspace = true } pallet-template.workspace = true +pallet-offchain-worker = { workspace = true } [build-dependencies] substrate-wasm-builder = { optional = true, workspace = true, default-features = true } @@ -72,6 +75,7 @@ std = [ "pallet-grandpa/std", "pallet-sudo/std", "pallet-template/std", + "pallet-offchain-worker/std", "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", @@ -89,7 +93,7 @@ std = [ "sp-storage/std", "sp-transaction-pool/std", "sp-version/std", - + "sp-std/std", "substrate-wasm-builder", ] @@ -116,6 +120,7 @@ try-runtime = [ "pallet-grandpa/try-runtime", "pallet-sudo/try-runtime", "pallet-template/try-runtime", + "pallet-offchain-worker/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "sp-runtime/try-runtime", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a5f8820..4c1630e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,10 +11,11 @@ use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - traits::{BlakeTwo256, Block as BlockT, IdentifyAccount, NumberFor, One, Verify}, + traits::{BlakeTwo256, Block as BlockT, Extrinsic, IdentifyAccount, NumberFor, One, Verify}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, MultiSignature, }; + #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; @@ -47,6 +48,7 @@ pub use sp_runtime::{Perbill, Permill}; /// Import the template pallet. pub use pallet_template; +pub use pallet_offchain_worker; /// An index to a block. pub type BlockNumber = u32; @@ -116,7 +118,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { /// up by `pallet_aura` to implement `fn slot_duration()`. /// /// Change this to adjust the block time. -pub const MILLISECS_PER_BLOCK: u64 = 6000; +pub const MILLISECS_PER_BLOCK: u64 = 12000; // NOTE: Currently it is not possible to change the slot duration after the chain has started. // Attempting to do so will brick block production. @@ -253,6 +255,79 @@ impl pallet_template::Config for Runtime { type WeightInfo = pallet_template::weights::SubstrateWeight; } +parameter_types! { + pub const UnsignedPriority: u64 = 1 << 20; +} + +impl pallet_offchain_worker::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AuthorityId = pallet_offchain_worker::crypto::TestAuthId; + type GracePeriod = ConstU32<5>; + type UnsignedInterval = ConstU32<2>; + type UnsignedPriority = UnsignedPriority; + type MaxPrices = ConstU32<64>; +} + +use codec::Encode; +use sp_runtime::{generic::Era, SaturatedConversion}; + + +impl frame_system::offchain::CreateSignedTransaction for Runtime + where + RuntimeCall: From, +{ + fn create_transaction>( + call: RuntimeCall, + public: ::Signer, + account: AccountId, + nonce: Nonce, + ) -> Option<(RuntimeCall, ::SignaturePayload)> { + let tip = 0; + // take the biggest period possible. + let period = + BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2) as u64; + let current_block = System::block_number() + .saturated_into::() + // The `System::block_number` is initialized with `n+1`, + // so the actual block number is `n`. + .saturating_sub(1); + let era = Era::mortal(period, current_block); + let extra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(era), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + ); + let raw_payload = SignedPayload::new(call, extra) + .map_err(|e| { + log::warn!("Unable to create signed payload: {:?}", e); + }) + .ok()?; + let signature = raw_payload.using_encoded(|payload| C::sign(payload, public))?; + let address = account; + let (call, extra, _) = raw_payload.deconstruct(); + Some((call, (sp_runtime::MultiAddress::Id(address), signature, extra))) + } +} + + +impl frame_system::offchain::SigningTypes for Runtime { + type Public = ::Signer; + type Signature = Signature; +} + +impl frame_system::offchain::SendTransactionTypes for Runtime + where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = RuntimeCall; +} + // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { @@ -294,6 +369,9 @@ mod runtime { // Include the custom logic from the pallet-template in the runtime. #[runtime::pallet_index(7)] pub type TemplateModule = pallet_template; + + #[runtime::pallet_index(8)] + pub type OffchainWorker = pallet_offchain_worker; } /// The address format for describing accounts.