03 Resources
3.7 Building a Token Sale
Even though we’re only halfway through the course, there’s already a lot that we’ve learned for you to design your first set of blueprints. This section will provide a tutorial on how to build a Token Sale and the main takeaway from this section are:
- Apply what you learned about creating your first resource (tokens).
- Get a feel of how resources are moved around.
- Build an intuition of using resource containers.
- Designing logic in our blueprint so our tokens can come to life!
//Objectives
We want to build a Token Sale that essentially gives us a useful token when we purchase it. So our design objectives are:
- Create a useful token
- Allow us to set a price to sell our tokens.
- Allow us to buy token(s)
//Defining our Blueprint
Let’s start with a blank canvass by having an empty blueprint. I’ve taken the liberty to name my blueprint TokenSale, so you can do yours as well
use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { } impl TokenSale { pub fn instantiate_token_sale() -> Global<TokenSale> { Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}When we think of a Token Sale, what is it that our component will need? We just finished our chapter on Resources, so perhaps we can consider what tokens this component will hold.
//Creating our Resource(s)
Of course, we do need our useful tokens to distribute! So we can start by creating a resource for our useful token.
use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { } impl TokenSale { pub fn instantiate_token_sale() -> Global<TokenSale> { // create a new useful token resource, with a fixed quantity of 100 let bucket_of_useful_tokens = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Useful Token", locked; "symbol" => "USEFUL", locked; } )) .mint_initial_supply(100); Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}//Defining our Struct
Some people prefer to start their blank canvass by working on the
Finally, the last thing our component needs to know is the price of the useful token we’re selling. This is what I have in my
struct of their blueprint first. After all, it define the state that the component will hold. In our case, since we created our useful tokens with an initial supply of 100, we’ll need a FungibleVault to hold it. Another thing to think about while we're in this section is that we’re going to need a way to collect payments for the useful tokens we sell, so we’ll need another FungibleVault to hold those tokens.Finally, the last thing our component needs to know is the price of the useful token we’re selling. This is what I have in my
struct to represent these.use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { useful_tokens_vault: FungibleVault, collected_xrd: FungibleVault, price_per_token: Decimal, } impl TokenSale { pub fn instantiate_token_sale() -> Global<TokenSale> { // create a new useful token resource, with a fixed quantity of 100 let bucket_of_useful_tokens = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Useful Token", locked; "symbol" => "USEFUL", locked; } )) .mint_initial_supply(100); Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}//Setting up our Component Instantiation Function
Now that we’ve defined our
struct we now need to implement how that will be handled at instantiation. We for example need to determine what tokens our vaults will hold.use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { useful_tokens_vault: FungibleVault, collected_xrd: FungibleVault, price_per_token: Decimal, } impl TokenSale { pub fn instantiate_token_sale() -> Global<TokenSale> { // create a new useful token resource, with a fixed quantity of 100 let bucket_of_useful_tokens = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Useful Token", locked; "symbol" => "USEFUL", locked; } )) .mint_initial_supply(100); Self { useful_tokens_vault: FungibleVault::with_bucket(bucket_of_useful_tokens), collected_xrd: FungibleVault::new(XRD), price_per_token: price_per_token, } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}Line 25: Notice that the way we want to instantiate this vault is by calling
Line 26: We have the variable
.new() which doesn’t accept FungibleBucket as an argument, but rather a ResourceAddress. Since we will be expecting our payments to be received through this component, we want to create a FungibleVault that receives the payment that we want. In this case the XRD. XRD is the globally recognized ResourceAddress of XRD, the native token of the Radix Network. We want to specify that XRD is the payment we want to receive to ensure that whatever payment we receive will have to be in XRD or else the component won’t accept the payment.Line 26: We have the variable
price as the value, which will be explained more shortly.//Completing our Instantiation Function
Now that we’ve determined how we want our component to be instantiated, let’s wrap up our instantiation function. As we noted earlier, we took a variable
price as a value in our component instantiation. This value will be a Decimal and will be determined when we first instantiate our component and essentially determines the price that we want to sell our useful tokens.use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { useful_tokens_vault: FungibleVault, collected_xrd: FungibleVault, price_per_token: Decimal, } impl TokenSale { pub fn instantiate_token_sale(price_per_token: Decimal) -> Global<TokenSale> { // create a new useful token resource, with a fixed quantity of 100 let bucket_of_useful_tokens = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Useful Token", locked; "symbol" => "USEFUL", locked; } )) .mint_initial_supply(100); Self { useful_tokens_vault: FungibleVault::with_bucket(bucket_of_useful_tokens), collected_xrd: FungibleVault::new(XRD), price_per_token: price_per_token, } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}//Defining Methods
We now have a proper scaffolding for our TokenSale blueprint. When we instantiate our TokenSale component, we are able to create our supply of useful tokens, determine the price of our useful tokens, and have an ability to collect our XRD payments…
…yet, we have no way of purchasing any useful tokens! Here will be where we will create a method to purchase useful tokens.
When we think about a method to purchase useful tokens, of course we will want to accept a payment. So we will set up our method this way.
…yet, we have no way of purchasing any useful tokens! Here will be where we will create a method to purchase useful tokens.
When we think about a method to purchase useful tokens, of course we will want to accept a payment. So we will set up our method this way.
pub fn buy_useful_token(&mut self, payment: FungibleBucket) { }- We want to access and change the state of our component so one of our argument will require a
&mut self. - Of course, we also need an argument that accepts our payment. This will be a
Buckettype since resources need to be moved with a resource container.
Our method logic will be quite simple, we just need to be able to take our payments and give the right amount of useful tokens for the payment received.
pub fn buy_useful_token(&mut self, payment: FungibleBucket) -> FungibleBucket { // Collects our payment self.collected_xrd.put(payment); // Return 1 useful token self.useful_tokens_vault.take(1)}This is pretty much all we need! But what if our customer accidentally sends us too much payments? Of course we’d want more money - why not? Although, our customers wouldn’t be happy and we definitely want to have a good customer experience when buying our useful tokens. So we can add a few more things to make sure that the customer receives any extra funds for overpayment.
pub fn buy_useful_token(&mut self, mut payment: FungibleBucket) -> (FungibleBucket, FungibleBucket) { // take our price in XRD out of the payment if the caller has sent too // few, or sent something other than XRD, they'll get a runtime error let our_share = payment.take(self.price_per_token); self.collected_xrd.put(our_share); // we could have simplified the above into a single line, like so: // self.collected_xrd.put(payment.take(self.price)); // return a tuple containing a useful token, plus whatever change is // left on the input payment (if any) if we're out of useful tokens to // give, we'll see a runtime error when we try to grab one (self.useful_tokens_vault.take(1), payment)}We’ve changed a few things here so let’s go over them:Line 1: We’ve made our
Line 4: Because our
Line 5: We instead take the variable we created
Line 13: We return the useful token and if their
Because of Scrypto’s native asset-oriented features, our business logic is much more concise and readable. This is because Scrypto’s asset-oriented approach allows us to manage asset easily!
payment mutable, because we want to be able to manage the FungibleBucket of payments when we receive it.- We also returned a tuple of buckets so that we can send the useful token back as well as any overpayments for the useful token.
Line 4: Because our
payment is mutable we can now create a logic that only takes the price that we determined our useful tokens were going to be sold for.Line 5: We instead take the variable we created
our_share to take only the right amount of XRD.Line 13: We return the useful token and if their
payment was too much, whatever that was left over in the FungibleBucket that was sent is returned back to them!Because of Scrypto’s native asset-oriented features, our business logic is much more concise and readable. This is because Scrypto’s asset-oriented approach allows us to manage asset easily!
//Full Implementation
use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { useful_tokens_vault: FungibleVault, collected_xrd: FungibleVault, price_per_token: Decimal, } impl TokenSale { pub fn instantiate_token_sale(price_per_token: Decimal) -> Global<TokenSale> { let bucket_of_useful_tokens = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Useful Token", locked; "symbol" => "USEFUL", locked; } )) .mint_initial_supply(100); Self { useful_tokens_vault: FungibleVault::with_bucket(bucket_of_useful_tokens), collected_xrd: FungibleVault::new(XRD), price_per_token: price_per_token, } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } pub fn buy_useful_token(&mut self, mut payment: FungibleBucket) -> (FungibleBucket, FungibleBucket) { let our_share = payment.take(self.price_per_token); self.collected_xrd.put(our_share); (self.useful_tokens_vault.take(1), payment) } }}//Test it using Resim
Do you remember our chapter 2.4 Compiling, Publishing, and Running Hello Blueprint? We will test our TokenSale Blueprint using Resim step-by-step. First, we recommend you use
resim reset to reset your local ledger dataCreate a New Account
resim show account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhmaComponent Address: account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhmaBlueprint ID: { package_address: package_sim1pkgxxxxxxxxxaccntxxxxxxxxxx000929625493xxxxxxxxxrn8jm6, blueprint_name: "Account" }Owned Fungible Resources: 1└─ resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3: 10000 Radix (XRD)Owned Non-fungibles Resources: 1└─ resource_sim1nfzf2h73frult99zd060vfcml5kncq3mxpthusm9lkglvhsr0guahy: 1 Owner Badge └─ #1#Metadata: 0You could use
resim show to show info about an address. In this case we will review our new account.jakemai@Jakes-MacBook-Pro hello % resim publish .......Success! New Package: package_sim1pk3cmat8st4ja2ms8mjqy2e9ptk8y6cx40v4qnfrkgnxcp2krkpr92Publish our Package
resim call-function package_sim1pk3cmat8st4ja2ms8mjqy2e9ptk8y6cx40v4qnfrkgnxcp2krkpr92 TokenSale instantiate_token_sale 10Transaction Status: COMMITTED SUCCESS...New Entities: 2└─ Component: component_sim1cp4qmcqlmtsqns8ckwjttvffjk4j4smkhlkt0qv94caftlj5u2xve2└─ Resource: resource_sim1t4h3kupr5l95w6ufpuysl0afun0gfzzw7ltmk7y68ks5ekqh4cpx9wInstantiate our Component
Let's recall the resim call-function command:
We can see that our
resim call-function We can see that our
instantiate_token_sale function has one argument price_per_token, after calling the function we will set the price we want, in our case we will set it to 10.resim call-method component_sim1cp4qmcqlmtsqns8ckwjttvffjk4j4smkhlkt0qv94caftlj5u2xve2 buy_useful_token resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3:10Transaction Status: COMMITTED SUCCESSTransaction Cost: 0.47715139794 XRDLogs: 0Events: 10Outputs: 5Balance Changes: 6New Entities: 0Call Method: buy_useful_token
Let's recall the resim call-method command:
We can see that our
resim call-method We can see that our
buy_useful_token function receives a FungibleBucket as a parameter, for this we will send 10 XRD as follows :amountresim show account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhmaComponent Address: account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhmaBlueprint ID: { package_address: package_sim1pkgxxxxxxxxxaccntxxxxxxxxxx000929625493xxxxxxxxxrn8jm6, blueprint_name: "Account" }Owned Fungible Resources: 2├─ resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3: 9990 Radix (XRD)└─ resource_sim1t4h3kupr5l95w6ufpuysl0afun0gfzzw7ltmk7y68ks5ekqh4cpx9w: 1 Useful Token (USEFUL)Owned Non-fungibles Resources: 1└─ resource_sim1nfzf2h73frult99zd060vfcml5kncq3mxpthusm9lkglvhsr0guahy: 1 Owner Badge └─ #1#Metadata: 0View Account Changes
We will check our account again to see the changes.