02 Getting started with Scrypto
2.3 The Anatomy of a Blueprint
We’ve mentioned that Scrypto is based on Rust. This means that we’re effectively working on top of a Rust environment using a combination of Scrypto and Rust concepts. If you look at the top of the
If you are completely new to programming and don’t know what any of this means, not to worry we will be working from the ground up.
lib.rs file within the src folder of our Hello blueprint, you’ll notice that there’s an import called use scrypto::prelude::*;, this is one of the dependencies that we stated in the Cargo.toml file. Now we’ve brought the Scrypto library into our file so that we can write Scrypto code on top of our Rust environment. Additionally, given that Scrypto is used for programming application on a distributed ledger, there are some limitations to the libraries and data types that we can use in our Rust environment. As such these pages might be a good reference to go back to every once in a while:If you are completely new to programming and don’t know what any of this means, not to worry we will be working from the ground up.
//Overview of the Hello Blueprint
As we go through this section, it’ll be useful to have some top-down context of the Hello blueprint that we are studying. The Hello blueprint is a simple blueprint which describes a component design where, when the component is instantiated from it, will essentially create a supply of “Hello Token”. This supply of Hello Token will then be deposited into its vault. The component has a single method called
free_token and when users call this method, they’ll receive a bucket with a Hello Token. There are a few Scrypto concepts that this blueprint demonstrates:- The use of blueprints to define the Hello component that is instantiated from it.
- Therefore, the distinction between blueprints and components.
- The elegance of creating a simple Hello Token resource with Scrypto.
- The use of resource containers to move and store resources such as buckets and vaults.
- The ease and use of managing resource movements with Scrypto.
So with this in mind, let’s break apart each part of the blueprint and see what asset-oriented code looks like.
//Blueprint Structure
Let’s go over the
lib.rs file in the src folder and look at the structure of the Hello blueprint. The most basic Scrypto blueprint can be implemented like this:use scrypto::prelude::*;#[blueprint]mod hello { struct Hello { // Some data your component will hold defined by the blueprint. } impl Hello { // Implementation of your component defined by the blueprint. }}Every Scrypto blueprint will require a
#[blueprint] attribute macro. This tells the Rust compiler when you build your code that there’s something special going on here which allows the Rust compiler to recognize and understand Scrypto code.Additionally, we also have something called a
mod where the #[blueprint] attribute is applied to. In Rust, a mod is used to declare a module, which is a way to organize and group related code together. So in other words, this tells the Rust compiler that within the mod, there’s special Scrypto code that it needs to understand. We’ve named the module mod hello to be inline with our blueprint name Hello, but technically, it can be named anything.Finally, we have a
struct and an impl. The struct and an impl comes with a name and both of them need to have the same name. Unlike the mod, the name you choose for the struct and impl are case sensitive; and in fact, the name you choose for your struct and impl is the official name of your blueprint. In our case, it’s the Hello blueprint. So everything within the mod is what constitutes a Scrypto blueprint. You may write Rust code outside of the mod, but they will not be considered a blueprint (with some exceptions that you don’t need to worry about right now).A blueprint definition must contain a
struct and an impl section with the name of the blueprint.In Rust, macros are generally used as a shortcut where it contains more code within the macro to execute some logic. The
#[blueprint] line for instance has logic within the macro that executes a code that wraps our struct and impl section to allow the Rust compiler know that we will be introducing Scrypto elements within this section. Visit this link to learn more about Rust macros.//The Makeup of a Blueprint
The
struct block is where you define the data and resources that will be present in the component that will be instantiated from your blueprint. Let’s take a practical look of what that means by taking a look at our Hello blueprint.struct Hello { // Define what resources and data will be managed by Hello components sample_vault: Vault,}Here, we’ve defined in the
The
Now within the
struct of our Hello blueprint that it will have a field called a sample_vault, this field will have a value of a vault type. This is because the Hello blueprint as we’ll later find, creates and mints a supply of Hello Tokens where it will be deposited into sample_vault. The
struct is an important section to carefully consider when designing the blueprint as the component instantiated from the blueprint will rely on data and resources that you impose to carry out its functionality. So it’s good practice to foresee what your component might need access to when stating what data and resources it will have in the struct. Now within the
impl block, or implementation block, is where you write the actual logic and functionalities that your component will have when instantiated from the blueprint.impl Hello { // Define the functions and methods which your component will use. // You can have any number of functions and methods for your component. // However, at the very least, you will need an "instantiation function" // to provide instructions how a component will be instantiated from its blueprint.}Inside this block, you will write the functions callable on the blueprint itself and the methods callable on the instantiated components. Functions and methods have logic contained within it that serves the theme of that function or method. We write functions and methods with an idea in mind of its purpose. Say it is a function or method that creates and mints a token. The function or method will have logic that is dedicated to execute such purpose.
It’s important to make a distinction between functions and methods:
When we say that functions don’t have access to any state, what we mean is that it doesn’t have access to the data or resources that’s contained within the
It’s important to make a distinction between functions and methods:
- Functions can be called on the blueprints and do not have access to any state.
- Methods on the other hand are callable on the instantiated components and are able to get data from the state and update or mutate it.
When we say that functions don’t have access to any state, what we mean is that it doesn’t have access to the data or resources that’s contained within the
struct that we defined. If we create a function which withdraws resources from the vault that we defined in the struct, it cannot work, since it does not have access to it. However, what functions can do is configure the initial state we defined within the struct. Functions that serve this purpose is called an instantiation function.//Instantiation Function
When we take a look at the Hello blueprint, we can see a function called
instantiate_hello. This is called an instantiation function. Instantiation function is unique to other potential functions that you may decide to write such that its job is to instantiate components from its blueprint with an initial state. Let’s see practically what that means.pub fn instantiate_hello() -> Global<Hello> { // Create a new token called "HelloToken," with a fixed supply of 1000, and put that supply into a bucket let my_bucket: Bucket = ResourceBuilder::new_fungible(OwnerRole::None) .divisibility(DIVISIBILITY_MAXIMUM) .metadata(metadata! { init { "name" => "HelloToken", locked; "symbol" => "HT", locked; } }) .mint_initial_supply(1000) .into(); // Instantiate a Hello component, populating its vault with our supply of 1000 HelloToken Self { sample_vault: Vault::with_bucket(my_bucket), } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize()}In this instantiation function, have a resource definition which creates our Hello Tokens with an initial supply of 1,000. Just right below that between Line 26-28 is a statement
Self. In this context, it’s used to work with the data defined within the struct. And within the block, we’re referencing the sample_vault field and now populating the FungibleVault we declared with the supply of Hello Tokens. This process indicates to us that when this component is instantiated from the Hello blueprint, it will start its life with 1,000 Hello Tokens held within sample_vault. Hence, why we call it an instantiation function.Customizing Instantiation Functions
Because Scrypto has this blueprint and components pattern, we can customize how components can be instantiated from blueprints through its instantiation function by allowing arguments to be passed in its parameters. Let’s see what that looks like:
pub fn instantiate_hello(supply: Decimal) -> Global<Hello> { // Create a new token called "HelloToken," with a fixed supply of 1000, and put that supply into a bucket let my_bucket: Bucket = ResourceBuilder::new_fungible(OwnerRole::None) .divisibility(DIVISIBILITY_MAXIMUM) .metadata(metadata!( init { "name" => "Hello Token", locked; "symbol" => "HT", locked; } )) .mint_initial_supply(supply) .into(); // Instantiate a Hello component, populating its vault with our supply of 1000 HelloToken Self { sample_vault: Vault::with_bucket(my_bucket), } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize()}Take a look at the green highlights which shows the changes we made to the previous snippet. Functions and methods have parameters which defines the input that the function or method can take when it is called. We’ve changed the parameter where it can take a
supply argument with a value of a Decimal type which allows those who wants to instantiate from our Hello blueprint, to determine how many supply of Hello Tokens is minted and deposited into the component’s vault. The beauty of this blueprint and component pattern is that it allows multiple components that can be instantiated from the Hello blueprint with different states. We can even take this further!Breaking up the Instantiation Function
We can also create regular functions to customize our instantiation function:
pub fn create_hello_tokens(supply: Decimal) { let my_bucket: Bucket = ResourceBuilder::new_fungible(OwnerRole::None) .divisibility(DIVISIBILITY_MAXIMUM) .metadata(metadata! { init { "name" => "HelloToken", locked; "symbol" => "HT", locked; } }) .mint_initial_supply(supply) .into(); Hello::instantiate_hello(my_bucket);}pub fn instantiate_hello(bucket_of_tokens: Bucket) -> Global<Hello> { // Instantiate a Hello component, populating its vault with our supply of 1000 HelloToken Self { sample_vault: Vault::with_bucket(bucket_of_tokens), } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize()}Notice here that within our
In this way, we don’t need to be bounded to the tokens defined within the blueprint and instead have our own custom supply of tokens to pass to the instantiation of the Hello component!
instantiate_hello function, we completely taken out the resource definition of our hello_tokens, but we also created another function called create_hello_tokens which takes a supply argument and within that function, we have the resource definition of hello_tokens. Another thing to notice is on Line 26 we made a function call within the function, specifically to the instantiate_hello function and passed in the bucket of hello_tokens.What are the implication of this? For one, we’ve created two paths to instantiate a component from the Hello blueprint. We can now have a choice to call the create_hello_tokens function where we can decide how much supply of hello_tokens we’d like to create with the instantiation of the component. Another way is to straight way call the instantiate_hello function with our own supply of tokens we either created elsewhere or from our own account.In this way, we don’t need to be bounded to the tokens defined within the blueprint and instead have our own custom supply of tokens to pass to the instantiation of the Hello component!
//Component Instantiation
Let’s finally move on to the rest of an instantiation function and closely look at the instantiation process.
Self { sample_vault: FungibleVault::with_bucket(hello_tokens),}.instantiate().prepare_to_globalize(OwnerRole::None).globalize()When components are instantiated from blueprints, they represent a running program of the blueprint with its own state and methods which can be called. However, even though a component is a running program of its blueprint, it’s important to note that this instantiated component is not yet a “global entity”. In other words, it does not (yet) have its own identifiable address.
This is because instantiated components can be owned by other components, otherwise known as “owned components”. This means that the owned components can only be called by the components that owns it; making the owned component a private and personal program. This is quite a niche and nuanced concept which we will explore in a more advanced course. However, it’s still important to seed this idea and understand component distinctions.
After a component instantiated, we can then choose to globalize a component, which gives the component its own identifiable address called a
This is because instantiated components can be owned by other components, otherwise known as “owned components”. This means that the owned components can only be called by the components that owns it; making the owned component a private and personal program. This is quite a niche and nuanced concept which we will explore in a more advanced course. However, it’s still important to seed this idea and understand component distinctions.
After a component instantiated, we can then choose to globalize a component, which gives the component its own identifiable address called a
ComponentAddress. Globalizing a component has its own process, because once blueprint becomes a component, the Radix Engine can attach “modules” to the component. This is like having add-ons to your globalized component.//Globalizing a Component
When a component is globalized, it is given an address and is now callable on the network. Because the global component is now recognizable to users of the network, this means users can reference its
ComponentAddress and retrieve information about the component or interact with it by making method calls. There is a preparatory step where we can expand more thoughtfully how we want a component to be globalized. This is denoted by calling .prepare_to_globalize() after a component is instantiated.Self { sample_vault: FungibleVault::with_bucket(hello_tokens),}.instantiate().prepare_to_globalize(OwnerRole::None).globalize()The
.prepare_to_globalize() method starts a procedure where we can configure and establish a “component owner”, if we choose to. Additionally, there are “component modules” which we can attach to our component to globalize with. These modules allow extra configurations to modify how our global component can be interacted with on a live network. The “component owner” and “component modules” are concepts that has complex nuances which we will discuss separately later in this chapter. The key takeaway is that globalizing a component comes with a process where we can thoughtfully configure how our components behave when interacted with users on the network.Finally, a component is fully globalized when we call the .globalize() method on our instantiated component.Self { sample_vault: FungibleVault::with_bucket(hello_tokens),}.instantiate().prepare_to_globalize(OwnerRole::None).globalize()A globalized component returns us a
Global object, which is just a strongly typed identifier, ComponentAddress, of our global component.//Component Method
Now we’re onto the last piece of the Hello blueprint, the
free_token method:// This is a method, because it needs a reference to self. Methods can only be called on componentspub fn free_token(&mut self) -> Bucket { info!( "My balance is: {} HelloToken. Now giving away a token!", self.sample_vault.amount() ); // If the semi-colon is omitted on the last line, the last value seen is automatically returned // In this case, a bucket containing 1 HelloToken is returned self.sample_vault.take(1)}The clear distinction between a method and a function is shown on Line 38 where we see mutable reference to
The method takes a mutable reference to its state because when 1 Hello Token is taken out of
self or &mut self in its parameter. Methods are always distinguished by whether it has a reference or a mutable reference to self. This indicates that the method has access to its state defined within the struct block. Within the free_token there's really two piece of logic that describes this method:- The
info!macro, which is a log that outputs a message whenfree_tokenis called. - The reference to
sample_vaultwhere the amount of 1 Hello Token will be taken out and returned to the caller.
The method takes a mutable reference to its state because when 1 Hello Token is taken out of
sample_vault on Line 45, it is changing its state. What originally was 1,000 supply of Hello Token contained within that vault will have one less supply when the free_token method is called for the first time. This 1 Hello Token is returned to us in a Bucket denoted on Line 38.//Full Implementation
I’m sure you already have the full implementation of the Hello blueprint right on your desktop, but for the sake of completeness, we have the full implementation here as well.
use scrypto::prelude::*;#[blueprint]mod hello { struct Hello { // Define what resources and data will be managed by Hello components sample_vault: Vault, } impl Hello { // Implement the functions and methods which will manage those resources and data // This is a function, and can be called directly on the blueprint once deployed pub fn instantiate_hello() -> Global<Hello> { // Create a new token called "HelloToken," with a fixed supply of 1000, and put that supply into a bucket let my_bucket: Bucket = ResourceBuilder::new_fungible(OwnerRole::None) .divisibility(DIVISIBILITY_MAXIMUM) .metadata(metadata! { init { "name" => "HelloToken", locked; "symbol" => "HT", locked; } }) .mint_initial_supply(1000) .into(); // Instantiate a Hello component, populating its vault with our supply of 1000 HelloToken Self { sample_vault: Vault::with_bucket(my_bucket), } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } // This is a method, because it needs a reference to self. Methods can only be called on components pub fn free_token(&mut self) -> Bucket { info!( "My balance is: {} HelloToken. Now giving away a token!", self.sample_vault.amount() ); // If the semi-colon is omitted on the last line, the last value seen is automatically returned // In this case, a bucket containing 1 HelloToken is returned self.sample_vault.take(1) } }}