From abb57c04522dc41856ecd5d6a0f8be8478ac7fa5 Mon Sep 17 00:00:00 2001 From: Richard Sentino Date: Sun, 9 Jun 2024 21:26:46 +1200 Subject: [PATCH 1/3] first cut pallet collectibles --- Cargo.lock | 172 +++++++------- Cargo.toml | 2 +- pallets/collectibles/Cargo.toml | 31 +++ pallets/collectibles/src/lib-ref.rs | 337 ++++++++++++++++++++++++++++ pallets/collectibles/src/lib.rs | 171 ++++++++++++++ 5 files changed, 631 insertions(+), 82 deletions(-) create mode 100644 pallets/collectibles/Cargo.toml create mode 100644 pallets/collectibles/src/lib-ref.rs create mode 100644 pallets/collectibles/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a5c55fe..16b9fcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,6 +1175,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "collectibles" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -2171,6 +2181,87 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flat1-network" +version = "0.0.0" +dependencies = [ + "clap", + "flat1-network-runtime", + "frame-benchmarking-cli", + "frame-system", + "futures", + "jsonrpsee", + "pallet-transaction-payment", + "pallet-transaction-payment-rpc", + "sc-basic-authorship", + "sc-cli", + "sc-client-api", + "sc-consensus", + "sc-consensus-aura", + "sc-consensus-grandpa", + "sc-executor", + "sc-network", + "sc-offchain", + "sc-rpc-api", + "sc-service", + "sc-telemetry", + "sc-transaction-pool", + "sc-transaction-pool-api", + "serde_json", + "sp-api", + "sp-block-builder", + "sp-blockchain", + "sp-consensus-aura", + "sp-consensus-grandpa", + "sp-core", + "sp-inherents", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-timestamp", + "substrate-build-script-utils", + "substrate-frame-rpc-system", + "try-runtime-cli", +] + +[[package]] +name = "flat1-network-runtime" +version = "0.0.0" +dependencies = [ + "frame-benchmarking", + "frame-executive", + "frame-support", + "frame-system", + "frame-system-benchmarking", + "frame-system-rpc-runtime-api", + "frame-try-runtime", + "pallet-aura", + "pallet-balances", + "pallet-grandpa", + "pallet-sudo", + "pallet-template", + "pallet-timestamp", + "pallet-transaction-payment", + "pallet-transaction-payment-rpc-runtime-api", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-block-builder", + "sp-consensus-aura", + "sp-consensus-grandpa", + "sp-core", + "sp-genesis-builder", + "sp-inherents", + "sp-offchain", + "sp-runtime", + "sp-session", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.9.0)", + "sp-storage 19.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.9.0)", + "sp-transaction-pool", + "sp-version", + "substrate-wasm-builder", +] + [[package]] name = "flate2" version = "1.0.28" @@ -4552,87 +4643,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" -[[package]] -name = "node-template" -version = "0.0.0" -dependencies = [ - "clap", - "frame-benchmarking-cli", - "frame-system", - "futures", - "jsonrpsee", - "node-template-runtime", - "pallet-transaction-payment", - "pallet-transaction-payment-rpc", - "sc-basic-authorship", - "sc-cli", - "sc-client-api", - "sc-consensus", - "sc-consensus-aura", - "sc-consensus-grandpa", - "sc-executor", - "sc-network", - "sc-offchain", - "sc-rpc-api", - "sc-service", - "sc-telemetry", - "sc-transaction-pool", - "sc-transaction-pool-api", - "serde_json", - "sp-api", - "sp-block-builder", - "sp-blockchain", - "sp-consensus-aura", - "sp-consensus-grandpa", - "sp-core", - "sp-inherents", - "sp-io", - "sp-keyring", - "sp-runtime", - "sp-timestamp", - "substrate-build-script-utils", - "substrate-frame-rpc-system", - "try-runtime-cli", -] - -[[package]] -name = "node-template-runtime" -version = "0.0.0" -dependencies = [ - "frame-benchmarking", - "frame-executive", - "frame-support", - "frame-system", - "frame-system-benchmarking", - "frame-system-rpc-runtime-api", - "frame-try-runtime", - "pallet-aura", - "pallet-balances", - "pallet-grandpa", - "pallet-sudo", - "pallet-template", - "pallet-timestamp", - "pallet-transaction-payment", - "pallet-transaction-payment-rpc-runtime-api", - "parity-scale-codec", - "scale-info", - "sp-api", - "sp-block-builder", - "sp-consensus-aura", - "sp-consensus-grandpa", - "sp-core", - "sp-genesis-builder", - "sp-inherents", - "sp-offchain", - "sp-runtime", - "sp-session", - "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.9.0)", - "sp-storage 19.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.9.0)", - "sp-transaction-pool", - "sp-version", - "substrate-wasm-builder", -] - [[package]] name = "nohash-hasher" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index e516eea..e91ea72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT-0" homepage = "https://substrate.io" [workspace] -members = ["node", "pallets/template", "runtime"] +members = ["node", "pallets/collectibles", "pallets/template", "runtime"] resolver = "2" [profile.release] panic = "unwind" diff --git a/pallets/collectibles/Cargo.toml b/pallets/collectibles/Cargo.toml new file mode 100644 index 0000000..c4e1a73 --- /dev/null +++ b/pallets/collectibles/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "collectibles" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +homepage.workspace = true +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lints] +workspace = true + +[dependencies] +# frame-support = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/polkadot-sdk.git", branch = "release-polkadot-v1.1.0" } +# frame-system = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/polkadot-sdk.git", branch = "release-polkadot-v1.1.0" } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.9.0", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.9.0", default-features = false } + +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.1.1", default-features = false, features = [ + "derive", +] } + +[features] +default = ["std"] +std = ["codec/std", "frame-support/std", "frame-system/std", "scale-info/std"] diff --git a/pallets/collectibles/src/lib-ref.rs b/pallets/collectibles/src/lib-ref.rs new file mode 100644 index 0000000..6d4c1fd --- /dev/null +++ b/pallets/collectibles/src/lib-ref.rs @@ -0,0 +1,337 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_support::traits::{Currency, Randomness}; + use frame_system::pallet_prelude::*; + + #[derive(Clone, Encode, Decode, PartialEq, Copy, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum Color { + Red, + Yellow, + Blue, + Green, + } + + type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[derive(Clone, Encode, Decode, PartialEq, Copy, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[scale_info(skip_type_params(T))] + pub struct Collectible { + // Unsigned integers of 16 bytes to represent a unique identifier + pub unique_id: [u8; 16], + // `None` assumes not for sale + pub price: Option>, + pub color: Color, + pub owner: T::AccountId, + } + + #[pallet::pallet] + //#[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type Currency: Currency; + type CollectionRandomness: Randomness>; + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + #[pallet::constant] + type MaximumOwned: Get; + } + + #[pallet::storage] + pub(super) type CollectiblesCount = StorageValue<_, u64, ValueQuery>; + + /// Maps the Collectible struct to the unique_id. + #[pallet::storage] + pub(super) type CollectibleMap = + StorageMap<_, Twox64Concat, [u8; 16], Collectible>; + + /// Track the collectibles owned by each account. + #[pallet::storage] + pub(super) type OwnerOfCollectibles = StorageMap< + _, + Twox64Concat, + T::AccountId, + BoundedVec<[u8; 16], T::MaximumOwned>, + ValueQuery, + >; + + // Pallet errors + #[pallet::error] + pub enum Error { + /// Each collectible must have a unique identifier + DuplicateCollectible, + /// An account can't exceed the `MaximumOwned` constant + MaximumCollectiblesOwned, + /// The total supply of collectibles can't exceed the u64 limit + BoundsOverflow, + /// The collectible doesn't exist + NoCollectible, + // You are not the owner + NotOwner, + /// Trying to transfer a collectible to yourself + TransferToSelf, + /// The bid is lower than the asking price. + BidPriceTooLow, + /// The collectible is not for sale. + NotForSale, + } + + // Pallet events + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new collectible was successfully created. + CollectibleCreated { collectible: [u8; 16], owner: T::AccountId }, + /// A collectible was successfully transferred. + TransferSucceeded { from: T::AccountId, to: T::AccountId, collectible: [u8; 16] }, + /// The price of a collectible was successfully set. + PriceSet { collectible: [u8; 16], price: Option> }, + /// A collectible was successfully sold. + Sold { + seller: T::AccountId, + buyer: T::AccountId, + collectible: [u8; 16], + price: BalanceOf, + }, + } + + // Pallet internal functions + impl Pallet { + // Generates and returns the unique_id and color + fn gen_unique_id() -> ([u8; 16], Color) { + // Create randomness + let random = T::CollectionRandomness::random(&b"unique_id"[..]).0; + + // Create randomness payload. Multiple collectibles can be generated in the same block, + // retaining uniqueness. + let unique_payload = ( + random, + frame_system::Pallet::::extrinsic_index().unwrap_or_default(), + frame_system::Pallet::::block_number(), + ); + + // Turns into a byte array + let encoded_payload = unique_payload.encode(); + let hash = frame_support::Hashable::blake2_128(&encoded_payload); + + // Generate Color + if hash[0] % 2 == 0 { + (hash, Color::Red) + } else { + (hash, Color::Yellow) + } + } + + // Function to mint a collectible + pub fn mint( + owner: &T::AccountId, + unique_id: [u8; 16], + color: Color, + ) -> Result<[u8; 16], DispatchError> { + // Create a new object + let collectible = + Collectible:: { unique_id, price: None, color, owner: owner.clone() }; + + // Check if the collectible exists in the storage map + ensure!( + !CollectibleMap::::contains_key(&collectible.unique_id), + Error::::DuplicateCollectible + ); + + // Check that a new collectible can be created + let count = CollectiblesCount::::get(); + let new_count = count.checked_add(1).ok_or(Error::::BoundsOverflow)?; + + // Append collectible to OwnerOfCollectibles map + OwnerOfCollectibles::::try_append(&owner, collectible.unique_id) + .map_err(|_| Error::::MaximumCollectiblesOwned)?; + + // Write new collectible to storage and update the count + CollectibleMap::::insert(collectible.unique_id, collectible); + CollectiblesCount::::put(new_count); + + // Deposit the "CollectibleCreated" event. + Self::deposit_event(Event::CollectibleCreated { + collectible: unique_id, + owner: owner.clone(), + }); + + // Returns the unique_id of the new collectible if this succeeds + Ok(unique_id) + } + + // Update storage to transfer collectible + pub fn do_transfer(collectible_id: [u8; 16], to: T::AccountId) -> DispatchResult { + // Get the collectible + let mut collectible = + CollectibleMap::::get(&collectible_id).ok_or(Error::::NoCollectible)?; + let from = collectible.owner; + + ensure!(from != to, Error::::TransferToSelf); + let mut from_owned = OwnerOfCollectibles::::get(&from); + + // Remove collectible from list of owned collectible. + if let Some(ind) = from_owned.iter().position(|&id| id == collectible_id) { + from_owned.swap_remove(ind); + } else { + return Err(Error::::NoCollectible.into()); + } + // Add collectible to the list of owned collectibles. + let mut to_owned = OwnerOfCollectibles::::get(&to); + to_owned + .try_push(collectible_id) + .map_err(|_id| Error::::MaximumCollectiblesOwned)?; + + // Transfer succeeded, update the owner and reset the price to `None`. + collectible.owner = to.clone(); + collectible.price = None; + + // Write updates to storage + CollectibleMap::::insert(&collectible_id, collectible); + OwnerOfCollectibles::::insert(&to, to_owned); + OwnerOfCollectibles::::insert(&from, from_owned); + + Self::deposit_event(Event::TransferSucceeded { from, to, collectible: collectible_id }); + Ok(()) + } + + // An internal function for purchasing a collectible + pub fn do_buy_collectible( + unique_id: [u8; 16], + to: T::AccountId, + bid_price: BalanceOf, + ) -> DispatchResult { + // Get the collectible from the storage map + let mut collectible = + CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + let from = collectible.owner; + ensure!(from != to, Error::::TransferToSelf); + let mut from_owned = OwnerOfCollectibles::::get(&from); + + // Remove collectible from owned collectibles. + if let Some(ind) = from_owned.iter().position(|&id| id == unique_id) { + from_owned.swap_remove(ind); + } else { + return Err(Error::::NoCollectible.into()); + } + // Add collectible to owned collectible. + let mut to_owned = OwnerOfCollectibles::::get(&to); + to_owned + .try_push(unique_id) + .map_err(|_id| Error::::MaximumCollectiblesOwned)?; + // Mutating state with a balance transfer, so nothing is allowed to fail after this. + if let Some(price) = collectible.price { + ensure!(bid_price >= price, Error::::BidPriceTooLow); + // Transfer the amount from buyer to seller + T::Currency::transfer( + &to, + &from, + price, + frame_support::traits::ExistenceRequirement::KeepAlive, + )?; + // Deposit sold event + Self::deposit_event(Event::Sold { + seller: from.clone(), + buyer: to.clone(), + collectible: unique_id, + price, + }); + } else { + return Err(Error::::NotForSale.into()); + } + + // Transfer succeeded, update the collectible owner and reset the price to `None`. + collectible.owner = to.clone(); + collectible.price = None; + // Write updates to storage + CollectibleMap::::insert(&unique_id, collectible); + OwnerOfCollectibles::::insert(&to, to_owned); + OwnerOfCollectibles::::insert(&from, from_owned); + Self::deposit_event(Event::TransferSucceeded { from, to, collectible: unique_id }); + Ok(()) + } + } + + // Pallet callable functions + #[pallet::call] + impl Pallet { + /// Create a new unique collectible. + /// + /// The actual collectible creation is done in the `mint()` function. + #[pallet::weight(0)] + pub fn create_collectible(origin: OriginFor) -> DispatchResult { + // Make sure the caller is from a signed origin + let sender = ensure_signed(origin)?; + + // Generate the unique_id and color using a helper function + let (collectible_gen_unique_id, color) = Self::gen_unique_id(); + + // Write new collectible to storage by calling helper function + Self::mint(&sender, collectible_gen_unique_id, color)?; + + Ok(()) + } + + /// Transfer a collectible to another account. + /// Any account that holds a collectible can send it to another account. + /// Transfer resets the price of the collectible, marking it not for sale. + #[pallet::weight(0)] + pub fn transfer( + origin: OriginFor, + to: T::AccountId, + unique_id: [u8; 16], + ) -> DispatchResult { + // Make sure the caller is from a signed origin + let from = ensure_signed(origin)?; + let collectible = + CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + ensure!(collectible.owner == from, Error::::NotOwner); + Self::do_transfer(unique_id, to)?; + Ok(()) + } + + /// Update the collectible price and write to storage. + #[pallet::weight(0)] + pub fn set_price( + origin: OriginFor, + unique_id: [u8; 16], + new_price: Option>, + ) -> DispatchResult { + // Make sure the caller is from a signed origin + let sender = ensure_signed(origin)?; + // Ensure the collectible exists and is called by the owner + let mut collectible = + CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + ensure!(collectible.owner == sender, Error::::NotOwner); + // Set the price in storage + collectible.price = new_price; + CollectibleMap::::insert(&unique_id, collectible); + + // Deposit a "PriceSet" event. + Self::deposit_event(Event::PriceSet { collectible: unique_id, price: new_price }); + Ok(()) + } + + /// Buy a collectible. The bid price must be greater than or equal to the price + /// set by the collectible owner. + #[pallet::weight(0)] + pub fn buy_collectible( + origin: OriginFor, + unique_id: [u8; 16], + bid_price: BalanceOf, + ) -> DispatchResult { + // Make sure the caller is from a signed origin + let buyer = ensure_signed(origin)?; + // Transfer the collectible from seller to buyer. + Self::do_buy_collectible(unique_id, buyer, bid_price)?; + Ok(()) + } + } +} diff --git a/pallets/collectibles/src/lib.rs b/pallets/collectibles/src/lib.rs new file mode 100644 index 0000000..971cabe --- /dev/null +++ b/pallets/collectibles/src/lib.rs @@ -0,0 +1,171 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_support::traits::{Currency, Randomness}; + use frame_system::pallet_prelude::*; + + #[derive(Clone, Encode, Decode, PartialEq, Copy, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum Color { + Red, + Blue, + Green, + Yellow, + } + + type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[derive(Clone, Encode, Decode, PartialEq, Copy, RuntimeDebug, TypeInfo, MaxEncodedLen)] // @TODO: need to review + #[scale_info(skip_type_params(T))] + pub struct Collectible { + // Unsigned integers of 16 bytes to represent a unique identifier + pub unique_id: [u8; 16], + // `None` assumes not for sale + pub price: Option>, + pub color: Color, + pub owner: T::AccountId, + } + + // @NOTE: more discussion `generate_store` + // - https://substrate.stackexchange.com/q/1354 + // - https://substrate.stackexchange.com/a/1323 + // #[pallet::generate_store(pub(super) trait Store)] + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type Currency: Currency; + type CollectionRandomness: Randomness>; + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + #[pallet::constant] + type MaximumOwned: Get; + } + + #[pallet::storage] + pub(super) type CollectiblesCount = StorageValue<_, u64, ValueQuery>; + + /// Maps the Collectible struct to the unique_id. + #[pallet::storage] + pub(super) type CollectibleMap = + StorageMap<_, Twox64Concat, [u8; 16], Collectible>; + + /// Track the collectibles owned by each account. + #[pallet::storage] + pub(super) type OwnerOfCollectibles = StorageMap< + _, + Twox64Concat, + T::AccountId, + BoundedVec<[u8; 16], T::MaximumOwned>, + ValueQuery, + >; + + #[pallet::error] + pub enum Error { + /// Each collectible must have a unique identifier + DuplicateCollectible, + /// An account can't exceed the `MaximumOwned` constant + MaximumCollectiblesOwned, + /// The total supply of collectibles can't exceed the `u64` limit + BoundsOverflow, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new collectible was successfully created + CollectibleCreated { collectible: [u8; 16], owner: T::AccountId }, + } + + /// Pallet internal function + impl Pallet { + // Generates and returns the unique_id and color + fn gen_unique_id() -> ([u8; 16], Color) { + // Create randomness + let random = T::CollectionRandomness::random(&b"unique_id"[..]).0; + + // Create randomness payload. Multiple collectibles can be generated in the same block, + // retaining uniqueness. + let unique_payload = ( + random, + frame_system::Pallet::::extrinsic_index().unwrap_or_default(), + frame_system::Pallet::::block_number(), + ); + + // Turns into a byte array + let encoded_payload = unique_payload.encode(); + let hash = frame_support::Hashable::blake2_128(&encoded_payload); + + // Generate Color + if hash[0] % 2 == 0 { + (hash, Color::Red) + } else { + (hash, Color::Yellow) + } + } + + // Function to mint a collectible + pub fn mint( + owner: &T::AccountId, + unique_id: [u8; 16], + color: Color, + ) -> Result<[u8; 16], DispatchError> { + // Create a new object + let collectible = + Collectible:: { unique_id, price: None, color, owner: owner.clone() }; + + // check if the collectible exists in the storage map + ensure!( + !CollectibleMap::::contains_key(&collectible.unique_id), + Error::::DuplicateCollectible + ); + + // check that a new collectible can be created + let count = CollectiblesCount::::get(); + let new_count = count.checked_add(1).ok_or(Error::::BoundsOverflow)?; + + // Append collectible to OwnerOfCollectibles map + OwnerOfCollectibles::::try_append(&owner, collectible.unique_id) + .map_err(|_| Error::::MaximumCollectiblesOwned)?; + + // Write new collectible to storage and update the count + CollectibleMap::::insert(collectible.unique_id, collectible); + CollectiblesCount::::put(new_count); + + // Deposit the "CollectibleCreated" event + Self::deposit_event(Event::CollectibleCreated { + collectible: unique_id, + owner: owner.clone(), + }); + + // Return the unique_id of the new collectible if this succeeds + Ok(unique_id) + } + } + + // Pallet callable functions + #[pallet::call] + impl Pallet { + /// Create a new unique collectible. + /// + /// The actual collectible creation is done in the `mint()` function. + #[pallet::weight(0)] + pub fn create_collectible(origin: OriginFor) -> DispatchResult { + // Make sure the caller is from a signed origin + let sender = ensure_signed(origin)?; + + // Generate the unique_id and color using a helper function + let (collectible_gen_unique_id, color) = Self::gen_unique_id(); + + // Write new collectible to storage by calling helper function + Self::mint(&sender, collectible_gen_unique_id, color)?; + + Ok(()) + } + } +} From 6213b786b147285ac261d287e3524696d782b87c Mon Sep 17 00:00:00 2001 From: Richard Sentino Date: Tue, 11 Jun 2024 23:49:21 +1200 Subject: [PATCH 2/3] add transfer and set_price --- pallets/collectibles/src/lib.rs | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/pallets/collectibles/src/lib.rs b/pallets/collectibles/src/lib.rs index 971cabe..f7090ea 100644 --- a/pallets/collectibles/src/lib.rs +++ b/pallets/collectibles/src/lib.rs @@ -73,6 +73,12 @@ pub mod pallet { MaximumCollectiblesOwned, /// The total supply of collectibles can't exceed the `u64` limit BoundsOverflow, + /// The collectible doesn't exist + NoCollectible, + /// You are not the owner + NotOwner, + /// Trying to transfer a collectible to yourself + TransferToSelf, } #[pallet::event] @@ -80,6 +86,10 @@ pub mod pallet { pub enum Event { /// A new collectible was successfully created CollectibleCreated { collectible: [u8; 16], owner: T::AccountId }, + /// A collectible was successfully transferred. + TransferSucceeded { from: T::AccountId, to: T::AccountId, collectible: [u8; 16] }, + /// The price of a collectible was successfully set. + PriceSet { collectible: [u8; 16], price: Option> }, } /// Pallet internal function @@ -146,6 +156,41 @@ pub mod pallet { // Return the unique_id of the new collectible if this succeeds Ok(unique_id) } + + // Update storage to transfer collectible + pub fn do_transfer(collectible_id: [u8; 16], to: T::AccountId) -> DispatchResult { + // Get the collectible + let mut collectible = + CollectibleMap::::get(&collectible_id).ok_or(Error::::NoCollectible)?; + let from = collectible.owner; + + ensure!(from != to, Error::::TransferToSelf); + let mut from_owned = OwnerOfCollectibles::::get(&from); + + // Remove collectible from list of owned collectible. + if let Some(ind) = from_owned.iter().position(|&id| id == collectible_id) { + from_owned.swap_remove(ind); + } else { + return Err(Error::::NoCollectible.into()); + } + // Add collectible to the list of owned collectibles. + let mut to_owned = OwnerOfCollectibles::::get(&to); + to_owned + .try_push(collectible_id) + .map_err(|_id| Error::::MaximumCollectiblesOwned)?; + + // Transfer succeeded, update the owner and reset the price to `None`. + collectible.owner = to.clone(); + collectible.price = None; + + // Write updates to storage + CollectibleMap::::insert(&collectible_id, collectible); + OwnerOfCollectibles::::insert(&to, to_owned); + OwnerOfCollectibles::::insert(&from, from_owned); + + Self::deposit_event(Event::TransferSucceeded { from, to, collectible: collectible_id }); + Ok(()) + } } // Pallet callable functions @@ -167,5 +212,46 @@ pub mod pallet { Ok(()) } + + /// Transfer a collectible to another account. + /// Any account that holds a collectible can send it to another account. + /// Transfer resets the price of the collectible, marking it not for sale. + #[pallet::weight(0)] + pub fn transfer( + origin: OriginFor, + to: T::AccountId, + unique_id: [u8; 16], + ) -> DispatchResult { + // Make sure the caller is from a signed origin + let from = ensure_signed(origin)?; + let collectible = + CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + ensure!(collectible.owner == from, Error::::NotOwner); + Self::do_transfer(unique_id, to)?; + Ok(()) + } + + /// Update the collectible price and write to storage. + #[pallet::weight(0)] + pub fn set_price( + origin: OriginFor, + unique_id: [u8; 16], + new_price: Option>, + ) -> DispatchResult { + // Make sure the caller is from a signed origin + let sender = ensure_signed(origin)?; + // Ensure the collectible exists and is called by the owner + let mut collectible = CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + ensure!(collectible.owner == sender, Error::::NotOwner); + // Set the price in storage + collectible.price = new_price; + CollectibleMap::::insert(&unique_id, collectible); + + // Deposit a "PriceSet" event. + Self::deposit_event(Event::PriceSet { collectible: unique_id, price: new_price }); + Ok(()) + } } + + // @TODO https://docs.substrate.io/tutorials/collectibles-workshop/07-more-functions/ } From 1d8da1fff271a7b7c03503b4101a4d90ce83ff65 Mon Sep 17 00:00:00 2001 From: Richard Sentino Date: Thu, 20 Jun 2024 22:44:55 +1200 Subject: [PATCH 3/3] feat: add buying collectible --- pallets/collectibles/src/lib.rs | 91 ++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/pallets/collectibles/src/lib.rs b/pallets/collectibles/src/lib.rs index f7090ea..7225a56 100644 --- a/pallets/collectibles/src/lib.rs +++ b/pallets/collectibles/src/lib.rs @@ -79,6 +79,10 @@ pub mod pallet { NotOwner, /// Trying to transfer a collectible to yourself TransferToSelf, + /// The bid is lower than the asking price. + BidPriceTooLow, + /// The collectible is not for sale. + NotForSale, } #[pallet::event] @@ -90,6 +94,13 @@ pub mod pallet { TransferSucceeded { from: T::AccountId, to: T::AccountId, collectible: [u8; 16] }, /// The price of a collectible was successfully set. PriceSet { collectible: [u8; 16], price: Option> }, + /// A collectible was successfully sold. + Sold { + seller: T::AccountId, + buyer: T::AccountId, + collectible: [u8; 16], + price: BalanceOf, + }, } /// Pallet internal function @@ -191,6 +202,66 @@ pub mod pallet { Self::deposit_event(Event::TransferSucceeded { from, to, collectible: collectible_id }); Ok(()) } + + // An internal function for purchasing a colletible + pub fn do_buy_collectible( + unique_id: [u8; 16], + to: T::AccountId, + bid_price: BalanceOf, + ) -> DispatchResult { + // Get the collectible from the storage map + let mut collectible = + CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + let from = collectible.owner; + ensure!(from != to, Error::::TransferToSelf); + let mut from_owned = OwnerOfCollectibles::::get(&from); + + // Remove collectible from owned collectibles. + if let Some(ind) = from_owned.iter().position(|&id| id == unique_id) { + from_owned.swap_remove(ind); + } else { + return Err(Error::::NoCollectible.into()); + } + // Add collectible to owned collectible. + let mut to_owned = OwnerOfCollectibles::::get(&to); + to_owned + .try_push(unique_id) + .map_err(|_id| Error::::MaximumCollectiblesOwned)?; + + // Mutating state with a balance transfer, so nothing is allowed to fail after this. + if let Some(price) = collectible.price { + ensure!(bid_price >= price, Error::::BidPriceTooLow); + + // Transfer the amount from buyer to seller + T::Currency::transfer( + &to, + &from, + price, + frame_support::traits::ExistenceRequirement::KeepAlive, + )?; + + // Deposit sold event + Self::deposit_event(Event::Sold { + seller: from.clone(), + buyer: to.clone(), + collectible: unique_id, + price, + }); + } else { + return Err(Error::::NotForSale.into()); + } + + // Transfer succeeded, update the collectible owner and reset the price to `None` + collectible.owner = to.clone(); + collectible.price = None; + + // Write updates to storage + CollectibleMap::::insert(&unique_id, collectible); + OwnerOfCollectibles::::insert(&to, to_owned); + OwnerOfCollectibles::::insert(&from, from_owned); + Self::deposit_event(Event::TransferSucceeded { from, to, collectible: unique_id }); + Ok(()) + } } // Pallet callable functions @@ -241,7 +312,8 @@ pub mod pallet { // Make sure the caller is from a signed origin let sender = ensure_signed(origin)?; // Ensure the collectible exists and is called by the owner - let mut collectible = CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; + let mut collectible = + CollectibleMap::::get(&unique_id).ok_or(Error::::NoCollectible)?; ensure!(collectible.owner == sender, Error::::NotOwner); // Set the price in storage collectible.price = new_price; @@ -251,7 +323,22 @@ pub mod pallet { Self::deposit_event(Event::PriceSet { collectible: unique_id, price: new_price }); Ok(()) } + + /// Buy a collectible. The bid price must be greater than or equal to the price + /// set by the collectible owner. + #[pallet::weight(0)] + pub fn buy_collectible( + origin: OriginFor, + unique_id: [u8; 16], + bid_price: BalanceOf, + ) -> DispatchResult { + // Make sure the caller is from a signed origin + let buyer = ensure_signed(origin)?; + // Transfer the collectible from seller to buyer. + Self::do_buy_collectible(unique_id, buyer, bid_price)?; + Ok(()) + } } - // @TODO https://docs.substrate.io/tutorials/collectibles-workshop/07-more-functions/ + // @TODO https://docs.substrate.io/tutorials/collectibles-workshop/08-add-collectibles-to-runtime/ }