diff --git a/Cargo.lock b/Cargo.lock index f1365ec955..c9fa74a67f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5788,6 +5788,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-ipfs-template" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log 0.4.14", + "pallet-ipfs-core", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-lottery" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 147e1f8a40..ac8102a5b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ members = [ "frame/bags-list/fuzzer", "pallets/template", "pallets/ipfs-exmaple", + "pallets/ipfs-template", "pallets/ipfs-core", "pallets/pocket-mints", "primitives/api", diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index 9b3734f1d8..dbd4cfa71d 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -357,6 +357,13 @@ pub fn new_full_base( Some("//Alice"), ).expect("Creating key with account Alice should succeed"); + //ipfs-template + sp_keystore::SyncCryptoStore::sr25519_generate_new( + &*keystore, + sp_core::crypto::key_types::IPFS_EXAMPLE, + Some("//Alice"), + ).expect("Creating key with account Alice should succeed"); + //pocket-mints sp_keystore::SyncCryptoStore::sr25519_generate_new( &*keystore, diff --git a/pallets/ipfs-template/Cargo.toml b/pallets/ipfs-template/Cargo.toml new file mode 100644 index 0000000000..e362a9e814 --- /dev/null +++ b/pallets/ipfs-template/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = 'pallet-ipfs-template' +version = "4.0.0-dev" +description = "A template pallet for interacting with ipfs-core." +authors = ['Dan Henton '] +homepage = "https://substrate.io/" +edition = '2018' +license = "Unlicense" +publish = false +repository = "https://github.com/substrate-developer-hub/substrate-node-template/" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +frame-support = { default-features = false, version = "4.0.0-dev", path = "../../frame/support" } +frame-system = { default-features = false, version = "4.0.0-dev", path = "../../frame/system" } +frame-benchmarking = { default-features = false, version = "4.0.0-dev", path = "../../frame/benchmarking", optional = true } +log = { version = "0.4" } +sp-std = { default-features = false, version = "4.0.0-dev", path = "../../primitives/std" } +sp-core = { default-features = false, version = "4.0.0-dev", path = "../../primitives/core" } +sp-io = { default-features = false, version = "4.0.0-dev", path = "../../primitives/io" } +sp-runtime = { default-features = false, version = "4.0.0-dev", path = "../../primitives/runtime" } + +pallet-ipfs-core = { default-features = false, version = "4.0.0-dev", path = "../ipfs-core" } + +[features] +default = ['std'] +std = [ + 'codec/std', + 'scale-info/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'sp-std/std', + 'pallet-ipfs-core/std', +] + +runtime-benchmarks = ["frame-benchmarking"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/ipfs-template/README.md b/pallets/ipfs-template/README.md new file mode 100644 index 0000000000..8d751a4220 --- /dev/null +++ b/pallets/ipfs-template/README.md @@ -0,0 +1 @@ +License: Unlicense \ No newline at end of file diff --git a/pallets/ipfs-template/src/benchmarking.rs b/pallets/ipfs-template/src/benchmarking.rs new file mode 100644 index 0000000000..75fb5d141a --- /dev/null +++ b/pallets/ipfs-template/src/benchmarking.rs @@ -0,0 +1,11 @@ +//! Benchmarking setup for pallet-template + +use super::*; + +#[allow(unused)] +use crate::Pallet as Template; +use frame_benchmarking::{benchmarks, whitelisted_caller}; +use frame_system::RawOrigin; + +benchmarks! { +} diff --git a/pallets/ipfs-template/src/lib.rs b/pallets/ipfs-template/src/lib.rs new file mode 100644 index 0000000000..38a60245d4 --- /dev/null +++ b/pallets/ipfs-template/src/lib.rs @@ -0,0 +1,253 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +/// Edit this file to define custom logic or remove it if it is not needed. +/// Learn more about FRAME and the core library of Substrate FRAME pallets: +/// + +use codec::{ Decode, Encode }; +use frame_system::{ + offchain::{ AppCrypto, CreateSignedTransaction, SendSignedTransaction, Signer}, +}; +use sp_runtime::{ + offchain::{ + ipfs, + storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, + }, + RuntimeDebug +}; + +use frame_support::{ + traits::Randomness, + dispatch::DispatchResult, +}; + +#[cfg(feature = "std")] +use frame_support::serde::{ Deserialize, Serialize }; + +use sp_core::{ + offchain::{ Duration, IpfsRequest, IpfsResponse, OpaqueMultiaddr, Timestamp}, +}; +use log::{ error, info }; +use sp_std::str; +use sp_std::vec::Vec; +use core::{convert::TryInto}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod crypto { + use sp_core::{ + sr25519::Signature as Sr25519Signature, + }; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::Verify, + MultiSignature, MultiSigner + }; + + app_crypto!(sr25519, sp_core::crypto::key_types::IPFS_EXAMPLE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericPublic = sp_core::sr25519::Public; + type GenericSignature = sp_core::sr25519::Signature; + } + + // Implemented for mock runtime in tests + impl frame_system::offchain::AppCrypto<::Signer, Sr25519Signature> for TestAuthId { + type RuntimeAppPublic = Public; + type GenericPublic = sp_core::sr25519::Public; + type GenericSignature = sp_core::sr25519::Signature; + } +} + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_core::crypto::KeyTypeId; + use pallet_ipfs_core as IpfsCore; + use pallet_ipfs_core::{ + generate_id, ipfs_request, ocw_process_command, + ocw_parse_ipfs_response, addresses_to_utf8_safe_bytes, + IpfsCommand, CommandRequest, + Event as IpfsEvent, + Error as IpfsError, + }; + + pub const KEY_TYPE: KeyTypeId = sp_core::crypto::key_types::IPFS_EXAMPLE; + const PROCESSED_COMMANDS: &[u8; 24] = b"ipfs::ipfs-example-comds"; + + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_ipfs_core::Config + CreateSignedTransaction> { + /// The identifier type for an offchain worker. + type AuthorityId: AppCrypto; + + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type Event: From> + IsType<::Event>; + + /// overarching dispatch call type. + type Call: From>; + + type IpfsRandomness: Randomness; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + // The pallet's runtime storage items. + // https://docs.substrate.io/v3/runtime/storage + + /** Store a list of Commands for the ocw to process */ + // EXAMPLE: store the CommandRequests in a StorageValue for fast iteration. + #[pallet::storage] + #[pallet::getter(fn commands)] + pub type Commands = StorageValue<_, Vec>>; + + /** Pallets use events to inform users when important changes are made. + */ + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + OcwCallback(T::AccountId), + } + + /** Errors inform users that something went wrong. + - RequestFailed, + */ + #[pallet::error] + pub enum Error { + StorageError, + FailedToFindCid, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn offchain_worker(block_number: T::BlockNumber) { + if let Err(_err) = Self::ocw_process_command_requests(block_number) { error!("IPFS: command request"); } + } + } + + + // Dispatchable functions allows users to interact with the pallet and invoke state changes. + // These functions materialize as "extrinsics", which are often compared to transactions. + // Dispatchable functions must be annotated with a weight and must return a DispatchResult. + #[pallet::call] + impl Pallet { + // EXAMPLE: Add extrinsics here. + + /** Process a signed callback from the ocw */ + #[pallet::weight(0)] + pub fn ocw_callback(origin: OriginFor, identifier: [u8; 32], data: Vec) -> DispatchResult { + let signer = ensure_signed(origin)?; + + // Side effect:, swap_remove will change the ordering of the Vec! + // TODO: the lock still exists in the ocw storage + let mut callback_command :Option> = None; + Commands::::mutate(|command_requests| { + let mut commands = command_requests.clone().unwrap(); + + if let Some(index) = commands.iter().position(|cmd| { cmd.identifier == identifier }) { + info!("Removing at index {}", index.clone()); + callback_command = Some(commands.swap_remove(index).clone()); + }; + + *command_requests = Some(commands); + }); + + Self::deposit_event(Event::OcwCallback(signer)); + + Self::command_callback(&callback_command.unwrap(), data); + + Ok(()) + } + } + + impl Pallet { + /** + Iterate over all of the Active CommandRequests calling them. + + Im sure some more logic will go in here. + */ + fn ocw_process_command_requests(block_number: T::BlockNumber) -> Result<(), Error> { + let commands: Vec> = Commands::::get().unwrap_or(Vec::>::new()); + + for command_request in commands { + + match ocw_process_command::(block_number, command_request.clone(), PROCESSED_COMMANDS) { + Ok(responses) => { + let callback_response = ocw_parse_ipfs_response::(responses); + + // EXAMPLE: Some more logic can be put here. + + Self::signed_callback(&command_request, callback_response); + } + Err(e) => { + match e { + IpfsError::::RequestFailed => { error!("IPFS: failed to perform a request") }, + _ => {} + } + } + } + } + + Ok(()) + } + + /** callback to the on-chain validators to continue processing the CID **/ + fn signed_callback(command_request: &CommandRequest, data: Vec) -> Result<(), IpfsError> { + // TODO: Dynamic signers so that its not only the validator who can sign the request. + let signer = Signer::::all_accounts(); + if !signer.can_sign() { + error!("*** IPFS *** ---- No local accounts available. Consider adding one via `author_insertKey` RPC."); + return Err(IpfsError::::RequestFailed)? + } + + let results = signer.send_signed_transaction(|_account| { + Call::ocw_callback{ + identifier: command_request.identifier, + data: data.clone() + } + }); + for (account, result) in &results { + match result { + Ok(()) => { info!("callback sent") }, + Err(e) => { error!("Failed to submit transaction {:?}", e)} + } + } + // TODO: return a better Result + Ok(()) + } + + // - Ideally the callback function can be override by another pallet that is coupled to this one allowing for custom functionality. + // - data can be used for callbacks IE add a cid to the signer / uploader. + // - Validate a connected peer has the CID, and which peer has it etc. + // TODO: Result + fn command_callback(command_request: &CommandRequest, data: Vec) -> Result<(), Error>{ + let owner = &command_request.clone().requester; + + for command in command_request.clone().ipfs_commands { + match command { + // EXAMPLE: Callback logic goes in here. Matching pallet_ipfs_core::pallet::IpfsCommand's + _ => {} + } + } + + // TODO: return a better Result + Ok(()) + } + } +} diff --git a/pallets/ipfs-template/src/mock.rs b/pallets/ipfs-template/src/mock.rs new file mode 100644 index 0000000000..4532d3d09b --- /dev/null +++ b/pallets/ipfs-template/src/mock.rs @@ -0,0 +1,63 @@ +use crate as pallet_template; +use frame_support::parameter_types; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + TemplateModule: pallet_template::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl 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 = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); +} + +impl pallet_template::Config for Test { + type Event = Event; +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default().build_storage::().unwrap().into() +} diff --git a/pallets/ipfs-template/src/tests.rs b/pallets/ipfs-template/src/tests.rs new file mode 100644 index 0000000000..bc3b5fc27a --- /dev/null +++ b/pallets/ipfs-template/src/tests.rs @@ -0,0 +1,2 @@ +use crate::{mock::*, Error}; +use frame_support::{assert_noop, assert_ok}; diff --git a/primitives/core/src/crypto.rs b/primitives/core/src/crypto.rs index 94eb7b8459..36775d42d1 100644 --- a/primitives/core/src/crypto.rs +++ b/primitives/core/src/crypto.rs @@ -1012,6 +1012,7 @@ pub mod key_types { pub const IPFS: KeyTypeId = KeyTypeId(*b"ipfs"); pub const POCKET_MINTS: KeyTypeId = KeyTypeId(*b"pokm"); pub const OCW_EXAMPLE: KeyTypeId = KeyTypeId(*b"btc!"); + pub const IPFS_EXAMPLE: KeyTypeId = KeyTypeId(*b"ipex"); } #[cfg(test)]