- 23 Oct 2024
- 4 Minutes to read
- DarkLight
- PDF
Build a Candy Store
- Updated on 23 Oct 2024
- 4 Minutes to read
- DarkLight
- PDF
This is a good opportunity to introduce another new blueprint. This time we'll create a candy store with two products, candy tokens and chocolate egg non-fungibles that each have toys inside. We're also going to have not just an owner role, but manager and staff roles too, so the store owner doesn't have to run all the day to day of the store. This introduces a new concept, authorization roles, which we will provide an explanation for.
Contents
Authorization Roles
Authorization in Scrypto is handled with roles. Each component has 2 predefined roles, the Owner role and the Self role. In previous examples, we used the Owner role to restrict access to multiple methods on our gumball machines. Here we'll add two more custom roles, the Manger and the Staff roles.
Adding Roles
Additional roles are defined in the enable_method_auth!
macro at the top of
the blueprint code. We add the manager and staff roles here:
enable_method_auth! {
roles {
manager => updatable_by: [OWNER];
staff => updatable_by: [manager, OWNER];
},
The component methods can then be restricted to one or more roles:
methods {
buy_candy => PUBLIC;
buy_chocolate_egg => PUBLIC;
get_prices => PUBLIC;
set_candy_price => restrict_to: [manager, OWNER];
set_chocolate_egg_price => restrict_to: [manager, OWNER];
mint_staff_badge => restrict_to: [manager, OWNER];
restock_store => restrict_to: [staff, manager, OWNER];
withdraw_earnings => restrict_to: [OWNER];
}
}
Badge Creation
Along with the Owner badge we now need a Manger badge and a Staff badges. The Manger badge is just like the Owner badge, but with name
and symbol
metadata of Manger Badge
and MNG
respectively.
let manager_badge: Bucket = ResourceBuilder::new_fungible(OwnerRole::None)
.metadata(metadata!(
init {
"name" => "Manger Badge", locked;
"symbol" => "MNG", locked;
}
))
.divisibility(DIVISIBILITY_NONE)
.mint_initial_supply(1)
.into();
The Staff badge, as well as having it's own name
and symbol
, is mintable in case we hire more staff.
To make it mintable we've set the minter rule to require the component address as proof. To do that we have to reserve an address for the component at the beginning of the instantiate function.
let (address_reservation, component_address) =
Runtime::allocate_component_address(CandyStore::blueprint_id());
The component_address
can then be used in the minter
rule.
let staff_badge: Bucket = ResourceBuilder::new_fungible(OwnerRole::None)
.metadata(metadata!(
init {
"name" => "Staff Badge", locked;
"symbol" => "STAFF", locked;
}
))
.divisibility(DIVISIBILITY_NONE)
.mint_roles(mint_roles! {
// add component address to minter rule
minter => rule!(require(global_caller(component_address)));
minter_updater => rule!(deny_all);
})
.mint_initial_supply(2)
.into();
This means that only the instantiated CandyStore
component can mint the Staff badge, which can be done with the mint_staff_badge
method called from the component.
This is the Virtual Badge pattern described in Use the Gumball Machine on Stokenet.
The same minting rule is applied to our candy and chocolate egg tokens. This Scrypto code means that we'll know that they must have come from our buy_candy
and buy_chocolate_egg
methods, as only the CandyStore
component can mint them.
Instantiation
Now we have these new roles and rules, we need to instantiate the component with rules stating which proofs are required to fulfil which roles. We also need to give the new component it's address reserved at the beginning of the instantiation function.
let component = Self {
// --snip--
}
.prepare_to_globalize(OwnerRole::Fixed(rule!(require(
owner_badge.resource_address()
))))
// define required proofs for custom roles
.roles(roles!(
manager => rule!(require(manager_badge.resource_address()));
staff => rule!(require(staff_badge.resource_address()));))
// apply the address reservation to the component
.with_address(address_reservation)
.globalize();
Candy Store Methods
Buying a chocolate egg works the same as buying a gumball in previouse sections. You provide a payment and if it is more than the cost of an egg, you get one chocolate egg and change.
pub fn buy_chocolate_egg(&mut self, mut payment: Bucket) -> (Bucket, Bucket) {
Buying candy is a little different. You can buy as much candy as is currently in stock at once, if you have the tokens to pay for it. The payment is divided by the candy price and you receive that many candy tokens, plus any change. You get as much candy as you can afford for your XRD. Any change is returned with the candy
pub fn buy_candy(&mut self, mut payment: Bucket) -> (Bucket, Bucket) {
The buy_candy
method uses checked mathematical operations to prevent overflow which might lock a component. It is highly recommended that you do the same in any Decimal calculations.
let candy_amount = payment
.amount()
.checked_div(self.candy_price)
.unwrap()
.checked_round(0, RoundingMode::ToZero)
.unwrap();
See the Decimal Overflows section of the docs for more information.
There are also two methods to set the price of the candy and chocolate eggs. These are restricted to the manager and owner roles.
pub fn set_candy_price(&mut self, new_price: Decimal) {
pub fn set_chocolate_egg_price(&mut self, new_price: Decimal) {
Minting staff badges is also restricted to the manager and owner roles. The method returns the minted badge rather than storing it in the component.
pub fn mint_staff_badge(&mut self) -> Bucket {
The restock_store
method can be called by staff, manager or owner role holders. It now adds resources to two vaults, adding one of each type of egg and refilling the candy vault to 100.
pub fn restock_store(&mut self) {
let candy_amount = 100 - self.candy.amount();
self.candy
.put(self.candy_resource_manager.mint(candy_amount));
let eggs = [
Egg { toy: Toy::Dinosaur },
Egg { toy: Toy::Unicorn },
Egg { toy: Toy::Dragon },
Egg { toy: Toy::Robot },
Egg { toy: Toy::Pony },
];
// loop through eggs array
for egg in eggs.iter() {
self.chocolate_eggs.put(
// mint a non-fungible for each egg
self.chocolate_egg_resource_manager
.mint_ruid_non_fungible(egg.clone()),
)
}