diff --git a/Cargo.lock b/Cargo.lock index 9b8c4e5..6a89d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4081,6 +4081,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 = "lock_api" version = "0.4.11" @@ -4610,6 +4628,7 @@ dependencies = [ "sp-inherents", "sp-io", "sp-keyring", + "sp-keystore", "sp-runtime", "sp-timestamp", "substrate-build-script-utils", @@ -4628,6 +4647,7 @@ dependencies = [ "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", + "log", "node-primitives", "pallet-aura", "pallet-balances", @@ -4893,6 +4913,8 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "lite-json", + "log", "pallet-balances", "pallet-insecure-randomness-collective-flip", "pallet-nfts", diff --git a/node/Cargo.toml b/node/Cargo.toml index 96c6a15..45c8b62 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "node-template" version = "4.0.0-dev" -description = "A fresh FRAME-based Substrate node, ready for hacking." -authors = ["Substrate DevHub "] -homepage = "https://substrate.io/" +description = "The RealXDeal Substrate node." +authors = ["Xcavate Network"] +homepage = "https://xcavate.io" edition = "2021" -license = "MIT-0" +license = "Apache-2.0" publish = false -repository = "https://github.com/substrate-developer-hub/substrate-node-template/" +repository = "https://github.com/XcavateBlockchain/Node_Hackathon_Apr2024/" build = "build.rs" [package.metadata.docs.rs] @@ -35,6 +35,7 @@ sp-consensus-aura = { version = "0.10.0-dev", git = "https://github.com/parityte sc-consensus = { version = "0.10.0-dev", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } sc-consensus-grandpa = { version = "0.10.0-dev", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } sp-consensus-grandpa = { version = "4.0.0-dev", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } +sp-keystore = {git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } sc-client-api = { version = "4.0.0-dev", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } sp-runtime = { version = "24.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } sp-io = { version = "23.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } diff --git a/node/src/service.rs b/node/src/service.rs index c4a2b2f..df88269 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -66,6 +66,23 @@ pub fn new_partial( )?; let client = Arc::new(client); + let keystore = keystore_container.keystore(); + if config.offchain_worker.enabled { + // Initialize seed for signing transaction using off-chain workers. This is a convenience + // so learners can see the transactions submitted simply running the node. + // Typically these keys should be inserted with RPC calls to `author_insertKey`. + + // For pallet-ocw + sp_keystore::Keystore::sr25519_generate_new( + &*keystore, + node_template_runtime::pallet_game::KEY_TYPE, + Some("//Alice"), + ) + .expect("Creating key with account Alice should succeed."); + + // For pallet-example-offchain-worker + } + let telemetry = telemetry.map(|(worker, telemetry)| { task_manager.spawn_handle().spawn("telemetry", None, worker.run()); telemetry diff --git a/pallets/game/Cargo.toml b/pallets/game/Cargo.toml index a648016..5031ff8 100644 --- a/pallets/game/Cargo.toml +++ b/pallets/game/Cargo.toml @@ -2,12 +2,12 @@ name = "pallet-game" version = "4.0.0-dev" description = "Game pallet for defining game logic." -authors = ["Substrate DevHub "] -homepage = "https://substrate.io" +authors = ["Xcavate Network"] +homepage = "https://xcavate.io" edition = "2021" -license = "MIT-0" +license = "Apache-2.0" publish = false -repository = "https://github.com/substrate-developer-hub/substrate-node-template/" +repository = "https://github.com/XcavateBlockchain/Node_Hackathon_Apr2024" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] @@ -22,15 +22,18 @@ serde = { version = "1.0.197", features = ["derive"], optional = true } frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } -sp-std = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-v1.6.0" } +sp-io = { version = "23.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0", default-features = false } +sp-std = { version = "8.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0", default-features = false } +sp-core = { version = "21.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0", default-features = false } +sp-runtime = { version = "24.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0", default-features = false } pallet-nfts = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-v1.6.0" } enumflags2 = { version = "0.7.7" } +log = { version = "0.4.21", default-features = false } +lite-json = { version = "0.2.0", default-features = false } + [dev-dependencies] -sp-core = { version = "21.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } -sp-io = { version = "23.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } -sp-runtime = { version = "24.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } pallet-insecure-randomness-collective-flip = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-v1.6.0" } pallet-balances = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-v1.6.0" } @@ -46,6 +49,8 @@ std = [ "pallet-nfts/std", "pallet-balances/std", "sp-std/std", + "log/std", + "lite-json/std", ] runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] -try-runtime = ["frame-support/try-runtime"] +try-runtime = ["frame-support/try-runtime"] \ No newline at end of file diff --git a/pallets/game/src/benchmarking.rs b/pallets/game/src/benchmarking.rs index 1c6954c..cd436d5 100644 --- a/pallets/game/src/benchmarking.rs +++ b/pallets/game/src/benchmarking.rs @@ -5,8 +5,11 @@ use super::*; #[allow(unused)] use crate::Pallet as GameModule; use frame_benchmarking::v2::*; +use frame_support::{ + assert_ok, + traits::{OnFinalize, OnInitialize}, +}; use frame_system::RawOrigin; -use frame_support::{assert_ok, traits::{OnInitialize, OnFinalize}}; fn create_setup() -> T::AccountId { let caller: T::AccountId = whitelisted_caller(); @@ -16,8 +19,15 @@ fn create_setup() -> T::AccountId { } fn practise_round(caller: T::AccountId, game_id: u32) { - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Practice)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller.clone()).into(), 20, game_id)); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Practice + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller.clone()).into(), + 20, + game_id + )); } #[benchmarks] @@ -45,114 +55,148 @@ mod benchmarks { give_points(RawOrigin::Root, caller); } - #[benchmark] + #[benchmark] fn play_game() { - let caller = - create_setup::(); + let caller = create_setup::(); current_block::(30u32.into()); practise_round::(caller.clone(), 0); #[extrinsic_call] - play_game( - RawOrigin::Signed(caller), - crate::DifficultyLevel::Player - ); - } + play_game(RawOrigin::Signed(caller), crate::DifficultyLevel::Player); + } - #[benchmark] + #[benchmark] fn submit_answer() { - let caller = - create_setup::(); + let caller = create_setup::(); current_block::(30u32.into()); practise_round::(caller.clone(), 0); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Player)); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Player + )); #[extrinsic_call] - submit_answer( - RawOrigin::Signed(caller), - 200000, - 1, - ); - } + submit_answer(RawOrigin::Signed(caller), 200000, 1); + } #[benchmark] fn list_nft() { - let caller = - create_setup::(); + let caller = create_setup::(); current_block::(30u32.into()); practise_round::(caller.clone(), 0); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Player)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller.clone()).into(), 220000, 1)); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Player + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller.clone()).into(), + 220000, + 1 + )); #[extrinsic_call] - list_nft( - RawOrigin::Signed(caller), - 0.into(), - 0.into(), - ); - } + list_nft(RawOrigin::Signed(caller), 0.into(), 0.into()); + } #[benchmark] fn delist_nft() { - let caller = - create_setup::(); + let caller = create_setup::(); current_block::(30u32.into()); practise_round::(caller.clone(), 0); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Player)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller.clone()).into(), 220000, 1)); - assert_ok!(GameModule::::list_nft(RawOrigin::Signed(caller.clone()).into(), 0.into(), 0.into())); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Player + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller.clone()).into(), + 220000, + 1 + )); + assert_ok!(GameModule::::list_nft( + RawOrigin::Signed(caller.clone()).into(), + 0.into(), + 0.into() + )); #[extrinsic_call] - delist_nft( - RawOrigin::Signed(caller), - 0, - ); - } + delist_nft(RawOrigin::Signed(caller), 0); + } #[benchmark] fn make_offer() { - let caller = - create_setup::(); + let caller = create_setup::(); current_block::(30u32.into()); practise_round::(caller.clone(), 0); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Player)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller.clone()).into(), 220000, 1)); - assert_ok!(GameModule::::list_nft(RawOrigin::Signed(caller.clone()).into(), 0.into(), 0.into())); - let caller2 = - create_setup::(); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Player + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller.clone()).into(), + 220000, + 1 + )); + assert_ok!(GameModule::::list_nft( + RawOrigin::Signed(caller.clone()).into(), + 0.into(), + 0.into() + )); + let caller2 = create_setup::(); practise_round::(caller2.clone(), 2); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Player)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller.clone()).into(), 220000, 3)); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Player + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller.clone()).into(), + 220000, + 3 + )); #[extrinsic_call] - make_offer( - RawOrigin::Signed(caller2), - 0, - 0.into(), - 1.into(), - ); - } + make_offer(RawOrigin::Signed(caller2), 0, 0.into(), 1.into()); + } #[benchmark] fn handle_offer() { - let caller = - create_setup::(); + let caller = create_setup::(); current_block::(30u32.into()); practise_round::(caller.clone(), 0); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller.clone()).into(), crate::DifficultyLevel::Player)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller.clone()).into(), 220000, 1)); - assert_ok!(GameModule::::list_nft(RawOrigin::Signed(caller.clone()).into(), 0.into(), 0.into())); - let caller2 = - create_setup::(); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller.clone()).into(), + crate::DifficultyLevel::Player + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller.clone()).into(), + 220000, + 1 + )); + assert_ok!(GameModule::::list_nft( + RawOrigin::Signed(caller.clone()).into(), + 0.into(), + 0.into() + )); + let caller2 = create_setup::(); practise_round::(caller2.clone(), 2); - assert_ok!(GameModule::::play_game(RawOrigin::Signed(caller2.clone()).into(), crate::DifficultyLevel::Player)); - assert_ok!(GameModule::::submit_answer(RawOrigin::Signed(caller2.clone()).into(), 220000, 3)); - assert_eq!(GameModule::::users::>(caller2.clone()).unwrap().nfts.xorange, 1); - assert_ok!(GameModule::::make_offer(RawOrigin::Signed(caller2.clone()).into(), 0, 0.into(), 1.into())); + assert_ok!(GameModule::::play_game( + RawOrigin::Signed(caller2.clone()).into(), + crate::DifficultyLevel::Player + )); + assert_ok!(GameModule::::submit_answer( + RawOrigin::Signed(caller2.clone()).into(), + 220000, + 3 + )); + assert_eq!( + GameModule::::users::>(caller2.clone()).unwrap().nfts.xorange, + 1 + ); + assert_ok!(GameModule::::make_offer( + RawOrigin::Signed(caller2.clone()).into(), + 0, + 0.into(), + 1.into() + )); #[extrinsic_call] - handle_offer( - RawOrigin::Signed(caller2), - 0, - crate::Offer::Accept, - ); - } - + handle_offer(RawOrigin::Signed(caller2), 0, crate::Offer::Accept); + } + impl_benchmark_test_suite!(GameModule, crate::mock::new_test_ext(), crate::mock::Test); } @@ -169,4 +213,4 @@ fn current_block(new_block: frame_system::pallet_prelude::BlockNumber frame_system::Pallet::::on_initialize(frame_system::Pallet::::block_number()); GameModule::::on_initialize(frame_system::Pallet::::block_number()); } -} \ No newline at end of file +} diff --git a/pallets/game/src/lib.rs b/pallets/game/src/lib.rs index 03298f0..ef19d7e 100644 --- a/pallets/game/src/lib.rs +++ b/pallets/game/src/lib.rs @@ -15,7 +15,7 @@ mod tests; mod benchmarking; pub mod weights; pub use weights::*; - +pub mod offchain_function; pub mod properties; type AccountIdOf = ::AccountId; @@ -43,6 +43,59 @@ use enumflags2::BitFlags; use frame_support::traits::Randomness; +use codec::{Decode, Encode}; +use frame_support::traits::Get; +use frame_system::{ + self as system, + offchain::{ + AppCrypto, CreateSignedTransaction, SendSignedTransaction, + Signer, + }, + pallet_prelude::BlockNumberFor, +}; +use lite_json::json::JsonValue; +use scale_info::prelude::string::String; +use sp_core::crypto::KeyTypeId; + +use sp_runtime::{ + offchain::{ + http, + Duration, + }, + BoundedVec, RuntimeDebug, +}; +use sp_std::vec::Vec; + +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"btc!"); + +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; + } +} + #[frame_support::pallet] pub mod pallet { use super::*; @@ -137,19 +190,32 @@ pub mod pallet { pub item_id: ItemId, } + /// Struct to store the property data for a game. #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] - #[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] + #[derive( + Encode, + Decode, + Clone, + PartialEq, + Eq, + MaxEncodedLen, + frame_support::pallet_prelude::RuntimeDebugNoBound, + TypeInfo, + )] #[scale_info(skip_type_params(T))] pub struct PropertyInfoData { pub id: u32, - pub property_type: BoundedVec::StringLimit>, pub bedrooms: u32, pub bathrooms: u32, - pub city: BoundedVec::StringLimit>, - pub post_code: BoundedVec::StringLimit>, - pub key_features: BoundedVec::StringLimit>, + pub summary: BoundedVec::StringLimit>, + pub property_sub_type: BoundedVec::StringLimit>, + pub first_visible_date: BoundedVec::StringLimit>, + pub display_size: BoundedVec::StringLimit>, + pub display_address: BoundedVec::StringLimit>, + pub property_images1: BoundedVec::StringLimit>, } + /// Struct for the user datas. #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] #[scale_info(skip_type_params(T))] @@ -158,6 +224,7 @@ pub mod pallet { pub wins: u32, pub losses: u32, pub practise_rounds: u8, + pub last_played_round: u32, pub nfts: CollectedColors, } @@ -216,30 +283,38 @@ pub mod pallet { pub fn subtracting_calculate_points(&mut self, color: NftColor) -> u32 { match color { + NftColor::Xorange if self.nfts.xorange == 0 => 100, NftColor::Xorange if self.nfts.xorange == 1 => 120, - NftColor::Xorange if self.nfts.xorange == 2 => 100, - NftColor::Xorange if self.nfts.xorange == 3 => 120, + NftColor::Xorange if self.nfts.xorange == 2 => 220, + NftColor::Xorange if self.nfts.xorange == 3 => 340, + NftColor::Xpink if self.nfts.xpink == 0 => 100, NftColor::Xpink if self.nfts.xpink == 1 => 120, - NftColor::Xpink if self.nfts.xpink == 2 => 100, - NftColor::Xpink if self.nfts.xpink == 3 => 120, + NftColor::Xpink if self.nfts.xpink == 2 => 220, + NftColor::Xpink if self.nfts.xpink == 3 => 340, + NftColor::Xblue if self.nfts.xblue == 0 => 100, NftColor::Xblue if self.nfts.xblue == 1 => 120, - NftColor::Xblue if self.nfts.xblue == 2 => 100, - NftColor::Xblue if self.nfts.xblue == 3 => 120, + NftColor::Xblue if self.nfts.xblue == 2 => 220, + NftColor::Xblue if self.nfts.xblue == 3 => 340, + NftColor::Xcyan if self.nfts.xcyan == 0 => 100, NftColor::Xcyan if self.nfts.xcyan == 1 => 120, - NftColor::Xcyan if self.nfts.xcyan == 2 => 100, - NftColor::Xcyan if self.nfts.xcyan == 3 => 120, + NftColor::Xcyan if self.nfts.xcyan == 2 => 220, + NftColor::Xcyan if self.nfts.xcyan == 3 => 340, + NftColor::Xcoral if self.nfts.xcoral == 0 => 100, NftColor::Xcoral if self.nfts.xcoral == 1 => 120, - NftColor::Xcoral if self.nfts.xcoral == 2 => 100, - NftColor::Xcoral if self.nfts.xcoral == 3 => 120, + NftColor::Xcoral if self.nfts.xcoral == 2 => 220, + NftColor::Xcoral if self.nfts.xcoral == 3 => 340, + NftColor::Xpurple if self.nfts.xpurple == 0 => 100, NftColor::Xpurple if self.nfts.xpurple == 1 => 120, - NftColor::Xpurple if self.nfts.xpurple == 2 => 100, - NftColor::Xpurple if self.nfts.xpurple == 3 => 120, + NftColor::Xpurple if self.nfts.xpurple == 2 => 220, + NftColor::Xpurple if self.nfts.xpurple == 3 => 340, + NftColor::Xleafgreen if self.nfts.xleafgreen == 0 => 100, NftColor::Xleafgreen if self.nfts.xleafgreen == 1 => 120, - NftColor::Xleafgreen if self.nfts.xleafgreen == 2 => 100, - NftColor::Xleafgreen if self.nfts.xleafgreen == 3 => 120, + NftColor::Xleafgreen if self.nfts.xleafgreen == 2 => 220, + NftColor::Xleafgreen if self.nfts.xleafgreen == 3 => 340, + NftColor::Xgreen if self.nfts.xgreen == 0 => 100, NftColor::Xgreen if self.nfts.xgreen == 1 => 120, - NftColor::Xgreen if self.nfts.xgreen == 2 => 100, - NftColor::Xgreen if self.nfts.xgreen == 3 => 120, + NftColor::Xgreen if self.nfts.xgreen == 2 => 220, + NftColor::Xgreen if self.nfts.xgreen == 3 => 340, _ => 0, } } @@ -350,7 +425,9 @@ pub mod pallet { /// Configure the pallet by specifying the parameters and types on which it depends. #[pallet::config] - pub trait Config: frame_system::Config + pallet_nfts::Config //+ pallet_babe::Config + pub trait Config: + frame_system::Config + pallet_nfts::Config + //+ pallet_babe::Config { /// Because this pallet emits events, it depends on the runtime's definition of an event. type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -388,58 +465,77 @@ pub mod pallet { /// The maximum length of data stored in string. #[pallet::constant] type StringLimit: Get; + /// The maximum length of leaderboard. + #[pallet::constant] + type LeaderboardLimit: Get; } pub type CollectionId = ::CollectionId; pub type ItemId = ::ItemId; - #[pallet::storage] - #[pallet::getter(fn stored_hash)] - pub type StoredHash = StorageValue<_, T::Hash, OptionQuery>; - + /// The id of the current round. #[pallet::storage] #[pallet::getter(fn current_round)] pub(super) type CurrentRound = StorageValue<_, u32, ValueQuery>; + /// Bool if there is a round currently ongoing. #[pallet::storage] #[pallet::getter(fn round_active)] pub(super) type RoundActive = StorageValue<_, bool, ValueQuery>; + /// A mapping of a round to the winner of the round. #[pallet::storage] #[pallet::getter(fn round_champion)] pub(super) type RoundChampion = StorageMap<_, Blake2_128Concat, u32, AccountIdOf, OptionQuery>; + /// The next item id in a collection. #[pallet::storage] #[pallet::getter(fn next_color_id)] pub(super) type NextColorId = StorageMap<_, Blake2_128Concat, ::CollectionId, u32, ValueQuery>; + /// Mapping of a collection to the correlated color. #[pallet::storage] #[pallet::getter(fn collection_color)] pub(super) type CollectionColor = StorageMap<_, Blake2_128Concat, ::CollectionId, NftColor, OptionQuery>; + /// The next id of listings. #[pallet::storage] #[pallet::getter(fn next_lising_id)] pub(super) type NextListingId = StorageValue<_, u32, ValueQuery>; + /// The next id of offers. #[pallet::storage] #[pallet::getter(fn next_offer_id)] pub(super) type NextOfferId = StorageValue<_, u32, ValueQuery>; + /// The next id of game. #[pallet::storage] #[pallet::getter(fn game_id)] pub type GameId = StorageValue<_, u32, ValueQuery>; + /// The leaderboard of the game. + #[pallet::storage] + #[pallet::getter(fn leaderboard)] + pub type Leaderboard = StorageValue< + _, + BoundedVec<(AccountIdOf, u32), ::LeaderboardLimit>, + ValueQuery, + >; + + /// Mapping of an account id to the user data of the account. #[pallet::storage] #[pallet::getter(fn users)] pub type Users = StorageMap<_, Blake2_128Concat, AccountIdOf, User, OptionQuery>; + /// Mapping of game id to the game info. #[pallet::storage] #[pallet::getter(fn game_info)] pub type GameInfo = StorageMap<_, Blake2_128Concat, u32, GameData, OptionQuery>; + /// Mapping of listing id to the listing data. #[pallet::storage] #[pallet::getter(fn listings)] pub type Listings = StorageMap< @@ -450,6 +546,7 @@ pub mod pallet { OptionQuery, >; + /// Mapping of offer id to the offer data. #[pallet::storage] #[pallet::getter(fn offers)] pub type Offers = StorageMap< @@ -460,7 +557,7 @@ pub mod pallet { OptionQuery, >; - /// Stores the project keys and round types ending on a given block. + /// Stores the game keys and round types ending on a given block. #[pallet::storage] pub type GamesExpiring = StorageMap< _, @@ -481,8 +578,6 @@ pub mod pallet { #[pallet::getter(fn test_prices)] pub type TestPrices = StorageMap<_, Blake2_128Concat, u32, u32, OptionQuery>; - // Pallets use events to inform users when important changes are made. - // https://docs.substrate.io/main-docs/build/events-errors/ #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -547,6 +642,8 @@ pub mod pallet { CollectionUnknown, /// There is no active round at the moment. NoActiveRound, + /// The player is already registered. + PlayerAlreadyRegistered, } #[pallet::hooks] @@ -565,11 +662,16 @@ pub mod pallet { }); weight } + + fn offchain_worker(block_number: BlockNumberFor) { + log::info!("Hello World from offchain workers!"); + + let parent_hash = >::block_hash(block_number - 1u32.into()); + log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); + + } } - // 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 { /// Creates the setup for a new game. @@ -626,11 +728,13 @@ pub mod pallet { #[pallet::weight(::WeightInfo::register_user())] pub fn register_user(origin: OriginFor, player: AccountIdOf) -> DispatchResult { T::GameOrigin::ensure_origin(origin)?; + ensure!(Self::users(player.clone()).is_none(), Error::::PlayerAlreadyRegistered); let user = User { points: 50, wins: Default::default(), losses: Default::default(), practise_rounds: Default::default(), + last_played_round: Default::default(), nfts: CollectedColors::default(), }; Users::::insert(player.clone(), user); @@ -671,6 +775,12 @@ pub mod pallet { let signer = ensure_signed(origin)?; Self::check_enough_points(signer.clone(), game_type.clone())?; ensure!(Self::round_active(), Error::::NoActiveRound); + let mut user = Self::users(signer.clone()).ok_or(Error::::UserNotRegistered)?; + if Self::current_round() != user.last_played_round { + user.nfts == Default::default(); + user.last_played_round = user.last_played_round.checked_add(1).ok_or(Error::::ArithmeticOverflow)?; + Users::::insert(signer.clone(), user); + } let game_id = Self::game_id(); if game_type == DifficultyLevel::Player { let current_block_number = >::block_number(); @@ -698,7 +808,6 @@ pub mod pallet { })?; } let (hashi, _) = T::GameRandomness::random(&[(game_id % 256) as u8]); - StoredHash::::put(hashi); let u32_value = u32::from_le_bytes( hashi.as_ref()[4..8].try_into().map_err(|_| Error::::ConversionError)?, ); @@ -912,7 +1021,7 @@ pub mod pallet { /// /// Emits `OfferHandeld` event when succesfful. #[pallet::call_index(9)] - #[pallet::weight(::WeightInfo::handle_offer())] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn handle_offer(origin: OriginFor, offer_id: u32, offer: Offer) -> DispatchResult { let signer = ensure_signed(origin.clone())?; let offer_details = Offers::::take(offer_id).ok_or(Error::::OfferDoesNotExist)?; @@ -945,43 +1054,17 @@ pub mod pallet { )?; Listings::::take(offer_details.listing_id) .ok_or(Error::::ListingDoesNotExist)?; - let mut user_offer = Self::users(offer_details.owner.clone()) - .ok_or(Error::::UserNotRegistered)?; - let color_listing = Self::collection_color(listing_details.collection_id) - .ok_or(Error::::CollectionUnknown)?; - let color_offer = Self::collection_color(offer_details.collection_id) - .ok_or(Error::::CollectionUnknown)?; - user_offer.add_nft_color(color_listing.clone())?; - let points = user_offer.calculate_points(color_listing.clone()); - user_offer.points = - user_offer.points.checked_add(points).ok_or(Error::::ArithmeticOverflow)?; - user_offer.sub_nft_color(color_offer.clone())?; - let points = user_offer.subtracting_calculate_points(color_offer.clone()); - user_offer.points = - user_offer.points.checked_sub(points).ok_or(Error::::ArithmeticOverflow)?; - Users::::insert(offer_details.owner.clone(), user_offer.clone()); - - let mut user_listing = - Self::users(signer.clone()).ok_or(Error::::UserNotRegistered)?; - user_listing.add_nft_color(color_offer.clone())?; - let points = user_listing.calculate_points(color_offer); - user_listing.points = user_listing - .points - .checked_add(points) - .ok_or(Error::::ArithmeticOverflow)?; - user_listing.sub_nft_color(color_listing.clone())?; - let points = user_listing.subtracting_calculate_points(color_listing); - user_listing.points = user_listing - .points - .checked_sub(points) - .ok_or(Error::::ArithmeticUnderflow)?; - Users::::insert(signer.clone(), user_listing.clone()); - if user_listing.has_four_of_all_colors() { - Self::end_game(signer.clone()); - } - if user_offer.has_four_of_all_colors() { - Self::end_game(offer_details.owner.clone()); - } + + Self::swap_user_points( + offer_details.owner.clone(), + listing_details.collection_id, + offer_details.collection_id, + )?; + Self::swap_user_points( + signer.clone(), + offer_details.collection_id, + listing_details.collection_id, + )?; } else { pallet_nfts::Pallet::::do_transfer( offer_details.collection_id.into(), @@ -998,6 +1081,39 @@ pub mod pallet { Self::deposit_event(Event::::OfferHandeld { offer_id, offer }); Ok(()) } + + /// Add a new property and the price. + /// + /// The origin must be the sudo. + /// + /// Parameters: + /// - `property`: The new property that will be added. + /// - `price`: The price of the property that will be added. + #[pallet::call_index(10)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn add_property(origin: OriginFor, property: PropertyInfoData, price: u32) -> DispatchResult { + T::GameOrigin::ensure_origin(origin)?; + TestProperties::::try_append(property.clone()).map_err(|_| Error::::TooManyTest)?; + TestPrices::::insert(property.id, price); + Ok(()) + } + + /// Remove a new property and the price. + /// + /// The origin must be the sudo. + /// + /// Parameters: + /// - `id`: The id of the property that should be removed. + #[pallet::call_index(11)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn remove_property(origin: OriginFor, id: u32) -> DispatchResult { + T::GameOrigin::ensure_origin(origin)?; + let mut properties = TestProperties::::take(); + properties.retain(|property| property.id != id); + TestProperties::::put(properties); + TestPrices::::take(id); + Ok(()) + } } impl Pallet { @@ -1049,7 +1165,6 @@ pub mod pallet { match difference { 0..=10 => { let (hashi, _) = T::GameRandomness::random(&[game_id as u8]); - StoredHash::::put(hashi); let u32_value = u32::from_le_bytes( hashi.as_ref()[4..8] .try_into() @@ -1096,7 +1211,7 @@ pub mod pallet { .ok_or(Error::::ArithmeticOverflow)?; Users::::insert(game_info.player.clone(), user.clone()); if user.has_four_of_all_colors() { - Self::end_game(game_info.player.clone()); + Self::end_game(game_info.player.clone())?; } }, 11..=30 => { @@ -1160,7 +1275,6 @@ pub mod pallet { match difference { 0..=10 => { let (hashi, _) = T::GameRandomness::random(&[game_id as u8]); - StoredHash::::put(hashi); let u32_value = u32::from_le_bytes( hashi.as_ref()[4..8] .try_into() @@ -1207,7 +1321,7 @@ pub mod pallet { .ok_or(Error::::ArithmeticOverflow)?; Users::::insert(game_info.player.clone(), user.clone()); if user.has_four_of_all_colors() { - Self::end_game(game_info.player.clone()); + Self::end_game(game_info.player.clone())?; } }, 11..=30 => { @@ -1275,6 +1389,59 @@ pub mod pallet { user.practise_rounds.checked_add(1).ok_or(Error::::ArithmeticUnderflow)?; Users::::insert(game_info.player.clone(), user); } + let user = + Self::users(game_info.player.clone()).ok_or(Error::::UserNotRegistered)?; + Self::update_leaderboard(game_info.player, user.points)?; + Ok(()) + } + + fn update_leaderboard(user_id: AccountIdOf, new_points: u32) -> DispatchResult { + let mut leaderboard = Self::leaderboard(); + let leaderboard_size = leaderboard.len(); + + if let Some((_, user_points)) = leaderboard.iter_mut().find(|(id, _)| *id == user_id) { + *user_points = new_points; + leaderboard.sort_by(|a, b| b.1.cmp(&a.1)); + Leaderboard::::put(leaderboard); + return Ok(()); + } + if new_points > 0 && + (leaderboard_size < 10 || + new_points > leaderboard.last().map(|(_, points)| *points).unwrap_or(0)) + { + if leaderboard.len() >= 10 { + leaderboard.pop(); + } + leaderboard + .try_push((user_id, new_points)) + .map_err(|_| Error::::InvalidIndex)?; + leaderboard.sort_by(|a, b| b.1.cmp(&a.1)); + Leaderboard::::put(leaderboard); + } + Ok(()) + } + + fn swap_user_points( + nft_holder: AccountIdOf, + collection_id_add: CollectionId, + collection_id_sub: CollectionId, + ) -> DispatchResult { + let mut user = Self::users(nft_holder.clone()).ok_or(Error::::UserNotRegistered)?; + let color_add = + Self::collection_color(collection_id_add).ok_or(Error::::CollectionUnknown)?; + let color_sub = + Self::collection_color(collection_id_sub).ok_or(Error::::CollectionUnknown)?; + user.add_nft_color(color_add.clone())?; + let points = user.calculate_points(color_add); + user.points = user.points.checked_add(points).ok_or(Error::::ArithmeticOverflow)?; + user.sub_nft_color(color_sub.clone())?; + let points = user.subtracting_calculate_points(color_sub); + user.points = user.points.checked_sub(points).ok_or(Error::::ArithmeticOverflow)?; + Users::::insert(nft_holder.clone(), user.clone()); + Self::update_leaderboard(nft_holder.clone(), user.points)?; + if user.has_four_of_all_colors() { + Self::end_game(nft_holder)?; + } Ok(()) } diff --git a/pallets/game/src/mock.rs b/pallets/game/src/mock.rs index 904028f..b06cfe7 100644 --- a/pallets/game/src/mock.rs +++ b/pallets/game/src/mock.rs @@ -130,6 +130,7 @@ impl sp_core::Get for MaxProperties { parameter_types! { pub const GamePalletId: PalletId = PalletId(*b"py/rlxdl"); pub const MaxOngoingGame: u32 = 200; + pub const LeaderLimit: u32 = 10; } /// Configure the pallet-game in pallets/game. @@ -144,6 +145,7 @@ impl pallet_game::Config for Test { type MaxOngoingGames = MaxOngoingGame; type GameRandomness = RandomnessCollectiveFlip; type StringLimit = ConstU32<5000>; + type LeaderboardLimit = LeaderLimit; } // Build genesis storage according to the mock runtime. diff --git a/pallets/game/src/offchain_function.rs b/pallets/game/src/offchain_function.rs new file mode 100644 index 0000000..705d788 --- /dev/null +++ b/pallets/game/src/offchain_function.rs @@ -0,0 +1,399 @@ +use crate::*; +use frame_support::pallet_prelude::*; + +impl Pallet { + /// Fetch current price and return the result in cents. + pub fn fetch_property() -> Result, 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 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://ipfs.io/ipfs/QmZ3Dn5B2UMuv9PFr1Ba3NGSKft2rwToBKCPaCTCmSab4k?filename=testing_data.json" + ); + + // 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 = sp_std::str::from_utf8(&body).map_err(|_| { + log::warn!("No UTF8 body"); + http::Error::Unknown + })?; + + let property = match Self::parse_property(body_str) { + Some(property) => Ok(property), + None => { + log::warn!("Unable to extract price from the response: {:?}", body_str); + Err(http::Error::Unknown) + }, + }?; + + // log::warn!("Got property: {:?} cents", price); + + Ok(property) + } + + /// 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. + pub fn parse_property(property_str: &str) -> Option> { + let val = lite_json::parse_json(property_str); + let id = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'id' field in the first object + if let Some((_, v)) = + obj.into_iter().find(|(k, _)| k.iter().copied().eq("id".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::Number(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + let val = lite_json::parse_json(property_str); + let bedrooms = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'bedrooms' field in the first object + if let Some((_, v)) = + obj.into_iter().find(|(k, _)| k.iter().copied().eq("bedrooms".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::Number(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + let val = lite_json::parse_json(property_str); + let bathrooms = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'bathrooms' field in the first object + if let Some((_, v)) = + obj.into_iter().find(|(k, _)| k.iter().copied().eq("bathrooms".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::Number(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let val = lite_json::parse_json(property_str); + let summary = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'summary' field in the first object + if let Some((_, v)) = + obj.into_iter().find(|(k, _)| k.iter().copied().eq("summary".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::String(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let val = lite_json::parse_json(property_str); + let property_sub_type = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'propertySubType' field in the first object + if let Some((_, v)) = obj + .into_iter() + .find(|(k, _)| k.iter().copied().eq("propertySubType".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::String(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let val = lite_json::parse_json(property_str); + let first_visible_date = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'firstVisibleDate' field in the first object + if let Some((_, v)) = obj + .into_iter() + .find(|(k, _)| k.iter().copied().eq("firstVisibleDate".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::String(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let val = lite_json::parse_json(property_str); + let display_size = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'displaySize' field in the first object + if let Some((_, v)) = obj + .into_iter() + .find(|(k, _)| k.iter().copied().eq("displaySize".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::String(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let val = lite_json::parse_json(property_str); + let display_address = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'displayAddress' field in the first object + if let Some((_, v)) = obj + .into_iter() + .find(|(k, _)| k.iter().copied().eq("displayAddress".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::String(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let val = lite_json::parse_json(property_str); + let property_images = match val.ok()? { + JsonValue::Array(mut arr) => { + // Check if the array has at least one element + if let Some(obj) = arr.pop() { + // Check if the first element is an object + if let JsonValue::Object(obj) = obj { + // Find the 'propertyImages' field in the first object + if let Some((_, v)) = obj + .into_iter() + .find(|(k, _)| k.iter().copied().eq("propertyImages".chars())) + { + // Check if the value associated with 'id' is a number + if let JsonValue::String(number) = v { + number + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + } else { + return None; + } + }, + _ => return None, + }; + + let id = id.integer as u32; + let bedrooms = bedrooms.integer as u32; + let bathrooms = bathrooms.integer as u32; + let summary: &str = &summary.iter().collect::(); + let property_sub_type: &str = &property_sub_type.iter().collect::(); + let first_visible_date: &str = &first_visible_date.iter().collect::(); + let display_size: &str = &display_size.iter().collect::(); + let display_address: &str = &display_address.iter().collect::(); + let property_images: &str = &property_images.iter().collect::(); + + let property = PropertyInfoData { + id, + bedrooms, + bathrooms, + summary: summary.as_bytes().to_vec().try_into().unwrap(), + property_sub_type: property_sub_type.as_bytes().to_vec().try_into().unwrap(), + first_visible_date: first_visible_date.as_bytes().to_vec().try_into().unwrap(), + display_size: display_size.as_bytes().to_vec().try_into().unwrap(), + display_address: display_address.as_bytes().to_vec().try_into().unwrap(), + property_images1: property_images.as_bytes().to_vec().try_into().unwrap(), + }; + + Some(property) + + // Some(price.integer as u32 * 100 + (price.fraction / 10_u64.pow(exp)) as u32) + } + + /* pub fn fetch_property_and_send_signed() -> DispatchResult { + 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 property = Self::fetch_property().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 { property: property.clone() } + }); + + for (acc, res) in &results { + match res { + Ok(()) => log::info!("[{:?}] Submitted price of {:?} cents", acc.id, property), + Err(e) => log::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e), + } + } + + Ok(()) + } */ +} diff --git a/pallets/game/src/properties.rs b/pallets/game/src/properties.rs index 2a71368..a65aa02 100644 --- a/pallets/game/src/properties.rs +++ b/pallets/game/src/properties.rs @@ -2,67 +2,59 @@ use crate::*; use frame_support::pallet_prelude::*; impl Pallet { - pub(crate) fn create_test_properties() -> DispatchResult { + pub(crate) fn create_test_properties() -> DispatchResult { let new_property = PropertyInfoData { - id: 1, - property_type: "Apartment".as_bytes().to_vec().try_into().unwrap(), + id: 147229391, bedrooms: 2, - bathrooms: 2, - city: "Drays Yard, Norwich".as_bytes().to_vec().try_into().unwrap(), - post_code: "GB".as_bytes().to_vec().try_into().unwrap(), - key_features: "Second floor apartment located a short" - .as_bytes() - .to_vec() - .try_into() - .unwrap(), - }; - TestProperties::::try_append(new_property).map_err(|_| Error::::TooManyTest)?; - TestPrices::::insert(1, 220000); + bathrooms: 1, + summary: "Superb 2 double bedroom ground floor purpose-built apartment with sole use of garden. Directly opposite Hackney Downs Park, within walking distance of Clapton, Hackney Downs & Rectory Rd Stations. Benefitting from; 2 double bedrooms, fitted kitchen/diner, modern shower/WC, separate lounge with di...".as_bytes().to_vec().try_into().unwrap(), + property_sub_type: "Flat".as_bytes().to_vec().try_into().unwrap(), + first_visible_date: "2024-04-24T16:39:27Z".as_bytes().to_vec().try_into().unwrap(), + display_size: "".as_bytes().to_vec().try_into().unwrap(), + display_address: "St Peters Street, Islington".as_bytes().to_vec().try_into().unwrap(), + property_images1: "https://media.rightmove.co.uk/dir/crop/10:9-16:9/56k/55489/146480642/55489_2291824_IMG_00_0000_max_476x317.jpeg".as_bytes().to_vec().try_into().unwrap(), + }; + TestProperties::::try_append(new_property.clone()).map_err(|_| Error::::TooManyTest)?; + TestPrices::::insert(new_property.id, 220000); let new_property = PropertyInfoData { - id: 2, - property_type: "Apartment".as_bytes().to_vec().try_into().unwrap(), - bedrooms: 4, - bathrooms: 2, - city: "Norwich".as_bytes().to_vec().try_into().unwrap(), - post_code: "GB".as_bytes().to_vec().try_into().unwrap(), - key_features: "A historic and idiosyncratic Grade II" - .as_bytes() - .to_vec() - .try_into() - .unwrap(), - }; - TestProperties::::try_append(new_property).map_err(|_| Error::::TooManyTest)?; - TestPrices::::insert(2, 650000); + id: 146480642, + bedrooms: 2, + bathrooms: 1, + summary: "Exceptional, bright and spacious 915 sq ft period upper maisonette for sale with a balcony and 22'2 rear patio garden, presented in good condition and available chain free.".as_bytes().to_vec().try_into().unwrap(), + property_sub_type: "Flat".as_bytes().to_vec().try_into().unwrap(), + first_visible_date: "2024-04-24T16:39:27Z".as_bytes().to_vec().try_into().unwrap(), + display_size: "".as_bytes().to_vec().try_into().unwrap(), + display_address: "Aragon Tower, London, SE8".as_bytes().to_vec().try_into().unwrap(), + property_images1: "https://media.rightmove.co.uk/dir/crop/10:9-16:9/128k/127876/147229391/127876_33052394_IMG_12_0000_max_476x317.gif".as_bytes().to_vec().try_into().unwrap(), + }; + TestProperties::::try_append(new_property.clone()).map_err(|_| Error::::TooManyTest)?; + TestPrices::::insert(new_property.id, 650000); let new_property = PropertyInfoData { - id: 3, - property_type: "Town House".as_bytes().to_vec().try_into().unwrap(), + id: 147031382, bedrooms: 3, bathrooms: 2, - city: "Willow Lane, Norwich NR2".as_bytes().to_vec().try_into().unwrap(), - post_code: "GB".as_bytes().to_vec().try_into().unwrap(), - key_features: "A truly rare opportunity to secure" - .as_bytes() - .to_vec() - .try_into() - .unwrap(), - }; - TestProperties::::try_append(new_property).map_err(|_| Error::::TooManyTest)?; - TestPrices::::insert(3, 525000); + summary: "Exceptional, bright and spacious 915 sq ft period upper maisonette for sale with a balcony and 22'2 rear patio garden, presented in good condition and available chain free.".as_bytes().to_vec().try_into().unwrap(), + property_sub_type: "Flat".as_bytes().to_vec().try_into().unwrap(), + first_visible_date: "2024-04-24T16:39:27Z".as_bytes().to_vec().try_into().unwrap(), + display_size: "".as_bytes().to_vec().try_into().unwrap(), + display_address: "Aragon Tower, London, SE8".as_bytes().to_vec().try_into().unwrap(), + property_images1: "https://media.rightmove.co.uk/dir/crop/10:9-16:9/128k/127876/147229391/127876_33052394_IMG_12_0000_max_476x317.gif".as_bytes().to_vec().try_into().unwrap(), + }; + TestProperties::::try_append(new_property.clone()).map_err(|_| Error::::TooManyTest)?; + TestPrices::::insert(new_property.id, 525000); let new_property = PropertyInfoData { - id: 4, - property_type: "Apartment".as_bytes().to_vec().try_into().unwrap(), - bedrooms: 4, - bathrooms: 4, - city: "Trafalgar Street, Norwich".as_bytes().to_vec().try_into().unwrap(), - post_code: "GB".as_bytes().to_vec().try_into().unwrap(), - key_features: "A HIGHLY IMPRESSIVE BLOCK OF FOUR FLATS" - .as_bytes() - .to_vec() - .try_into() - .unwrap(), - }; - TestProperties::::try_append(new_property).map_err(|_| Error::::TooManyTest)?; - TestPrices::::insert(4, 500000); + id: 147031382, + bedrooms: 3, + bathrooms: 2, + summary: "Exceptional, bright and spacious 915 sq ft period upper maisonette for sale with a balcony and 22'2 rear patio garden, presented in good condition and available chain free.".as_bytes().to_vec().try_into().unwrap(), + property_sub_type: "Flat".as_bytes().to_vec().try_into().unwrap(), + first_visible_date: "2024-04-24T16:39:27Z".as_bytes().to_vec().try_into().unwrap(), + display_size: "".as_bytes().to_vec().try_into().unwrap(), + display_address: "Aragon Tower, London, SE8".as_bytes().to_vec().try_into().unwrap(), + property_images1: "https://media.rightmove.co.uk/dir/crop/10:9-16:9/128k/127876/147229391/127876_33052394_IMG_12_0000_max_476x317.gif".as_bytes().to_vec().try_into().unwrap(), + }; + TestProperties::::try_append(new_property.clone()).map_err(|_| Error::::TooManyTest)?; + TestPrices::::insert(new_property.id, 525000); Ok(()) - } + } } diff --git a/pallets/game/src/tests.rs b/pallets/game/src/tests.rs index 0ba4bd4..cc5034a 100644 --- a/pallets/game/src/tests.rs +++ b/pallets/game/src/tests.rs @@ -1,6 +1,7 @@ use crate::{mock::*, Error}; use frame_support::{assert_noop, assert_ok}; use sp_runtime::{traits::BadOrigin, DispatchError, ModuleError}; +use crate::PropertyInfoData; fn practise_round(player: AccountId, game_id: u32) { assert_ok!(GameModule::play_game( @@ -39,7 +40,7 @@ fn play_game_works() { RuntimeOrigin::signed([0; 32].into()), crate::DifficultyLevel::Player, )); - assert_eq!(GameModule::game_info(1).unwrap().property.id, 1); + assert_eq!(GameModule::game_info(1).unwrap().property.id, 147229391); }); } @@ -121,6 +122,41 @@ fn submit_answer_works() { }); } +#[test] +fn leaderboard_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(GameModule::setup_game(RuntimeOrigin::root())); + assert_eq!(GameModule::test_properties().len(), 4); + assert_ok!(GameModule::register_user(RuntimeOrigin::root(), [0; 32].into())); + assert_ok!(GameModule::register_user(RuntimeOrigin::root(), [1; 32].into())); + assert_ok!(GameModule::register_user(RuntimeOrigin::root(), [2; 32].into())); + practise_round([0; 32].into(), 0); + practise_round([1; 32].into(), 1); + practise_round([2; 32].into(), 2); + assert_ok!(GameModule::play_game( + RuntimeOrigin::signed([0; 32].into()), + crate::DifficultyLevel::Player, + )); + assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([0; 32].into()), 230_000, 3)); + assert_ok!(GameModule::play_game( + RuntimeOrigin::signed([1; 32].into()), + crate::DifficultyLevel::Player, + )); + assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([1; 32].into()), 225_000, 4)); + assert_ok!(GameModule::play_game( + RuntimeOrigin::signed([2; 32].into()), + crate::DifficultyLevel::Player, + )); + assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([2; 32].into()), 220_000, 5)); + assert_eq!(GameModule::game_info(0).is_none(), true); + assert_eq!(GameModule::users::([2; 32].into()).unwrap().points, 155); + assert_eq!(GameModule::users::([1; 32].into()).unwrap().points, 80); + assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 70); + assert_eq!(GameModule::leaderboard().len(), 3); + }); +} + #[test] fn submit_answer_fails() { new_test_ext().execute_with(|| { @@ -414,8 +450,8 @@ fn handle_offer_accept_works() { crate::DifficultyLevel::Player, )); assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([0; 32].into()), 220_000, 1)); - assert_eq!(GameModule::game_info(0).is_none(), true); assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 155); + assert_eq!(GameModule::game_info(0).is_none(), true); assert_eq!(Nfts::owner(0, 0).unwrap(), [0; 32].into()); practise_round([1; 32].into(), 2); assert_ok!(GameModule::play_game( @@ -423,6 +459,19 @@ fn handle_offer_accept_works() { crate::DifficultyLevel::Player, )); assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([1; 32].into()), 220_000, 3)); + assert_ok!(GameModule::play_game( + RuntimeOrigin::signed([0; 32].into()), + crate::DifficultyLevel::Player, + )); + assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([0; 32].into()), 220_000, 4)); + assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 275); + assert_ok!(GameModule::play_game( + RuntimeOrigin::signed([0; 32].into()), + crate::DifficultyLevel::Player, + )); + assert_ok!(GameModule::submit_answer(RuntimeOrigin::signed([0; 32].into()), 220_000, 5)); + assert_eq!(GameModule::users::([0; 32].into()).unwrap().nfts.xorange, 3); + assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 495); assert_eq!(GameModule::users::([1; 32].into()).unwrap().nfts.xorange, 1); assert_eq!(Nfts::owner(0, 1).unwrap(), [1; 32].into()); assert_ok!(GameModule::list_nft(RuntimeOrigin::signed([0; 32].into()), 0, 0,)); @@ -439,9 +488,9 @@ fn handle_offer_accept_works() { assert_eq!(Nfts::owner(0, 1).unwrap(), [0; 32].into()); assert_eq!(GameModule::offers(0).is_none(), true); assert_eq!(GameModule::listings(0).is_none(), true); - assert_eq!(GameModule::users::([0; 32].into()).unwrap().nfts.xorange, 1); + assert_eq!(GameModule::users::([0; 32].into()).unwrap().nfts.xorange, 3); assert_eq!(GameModule::users::([1; 32].into()).unwrap().nfts.xorange, 1); - assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 155); + assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 495); assert_eq!(GameModule::users::([1; 32].into()).unwrap().points, 155); assert_noop!( Nfts::transfer( @@ -590,3 +639,34 @@ fn play_multiple_rounds_works() { assert_eq!(GameModule::users::([0; 32].into()).unwrap().points, 555); }); } + +#[test] +fn add_property_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(GameModule::setup_game(RuntimeOrigin::root())); + let new_property = PropertyInfoData { + id: 146480642, + bedrooms: 2, + bathrooms: 1, + summary: "Spacious apartment in the heart of New York City with a balcony and garden.".as_bytes().to_vec().try_into().unwrap(), + property_sub_type: "Apartment".as_bytes().to_vec().try_into().unwrap(), + first_visible_date: "2024-05-06T12:00:00Z".as_bytes().to_vec().try_into().unwrap(), + display_size: "1000 sq ft".as_bytes().to_vec().try_into().unwrap(), + display_address: "New York City, NY".as_bytes().to_vec().try_into().unwrap(), + property_images1: "https://example.com/image1.jpg".as_bytes().to_vec().try_into().unwrap(), + }; + assert_ok!(GameModule::add_property(RuntimeOrigin::root(), new_property, 122565)); + assert_eq!(GameModule::test_properties().len(), 5); + }); +} + +#[test] +fn remove_property_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(GameModule::setup_game(RuntimeOrigin::root())); + assert_ok!(GameModule::remove_property(RuntimeOrigin::root(), 146480642)); + assert_eq!(GameModule::test_properties().len(), 3); + }); +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 7a1cf82..d61cfc6 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "node-template-runtime" version = "4.0.0-dev" -description = "A fresh FRAME-based Substrate node, ready for hacking." -authors = ["Substrate DevHub "] -homepage = "https://substrate.io/" +description = "The RealXDeal Substrate runtime." +authors = ["Xcavate Network"] +homepage = "https://xcavate.io" edition = "2021" -license = "MIT-0" +license = "Apache-2.0" publish = false -repository = "https://github.com/substrate-developer-hub/substrate-node-template/" +repository = "https://github.com/XcavateBlockchain/Node_Hackathon_Apr2024" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] @@ -19,6 +19,7 @@ codec = { package = "parity-scale-codec", version = "3.6.1", default-features = scale-info = { version = "2.5.0", default-features = false, features = [ "derive", ] } +log = { version = '0.4.14', default-features = false } sp-genesis-builder = { version = "0.1.0", default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.6.0" } @@ -69,6 +70,7 @@ substrate-wasm-builder = { version = "5.0.0-dev", git = "https://github.com/pari [features] default = ["std"] std = [ + "log/std", "frame-try-runtime?/std", "frame-system-benchmarking?/std", "frame-benchmarking?/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a2325ab..0718001 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,7 +11,7 @@ use constants::currency::*; use pallet_grandpa::AuthorityId as GrandpaId; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; +use sp_core::{crypto::KeyTypeId, Encode, OpaqueMetadata}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{BlakeTwo256, Block as BlockT, IdentifyAccount, NumberFor, One, Verify}, @@ -48,7 +48,7 @@ pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::{ConstFeeMultiplier, CurrencyAdapter, Multiplier}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; -pub use sp_runtime::{Perbill, Permill}; +pub use sp_runtime::{Perbill, Permill, SaturatedConversion}; /// Import the template pallet. pub use pallet_game; @@ -264,10 +264,11 @@ impl sp_core::Get for MaxProperties { } } -// Define the parameter types using the custom struct +// Define the parameter types using the custom struct. parameter_types! { pub const GamePalletId: PalletId = PalletId(*b"py/rlxdl"); pub const MaxOngoingGame: u32 = 200; + pub const LeaderLimit: u32 = 10; } /// Configure the pallet-game in pallets/game. @@ -282,6 +283,7 @@ impl pallet_game::Config for Runtime { type MaxOngoingGames = MaxOngoingGame; type GameRandomness = RandomnessCollectiveFlip; type StringLimit = StringLimit; + type LeaderboardLimit = LeaderLimit; } parameter_types! { @@ -334,6 +336,65 @@ impl pallet_nfts::Config for Runtime { impl pallet_insecure_randomness_collective_flip::Config for Runtime {} +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 extra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(generic::Era::mortal(period, current_block)), + 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: MultiSignature = + 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.into(), 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 OverarchingCall = RuntimeCall; + type Extrinsic = UncheckedExtrinsic; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub struct Runtime {