🏎️
Test Content
  • HomePage
  • 🪙Trustless Rewards
    • Intro and overall idea
    • Flow Lobbies
    • Smart Contract
  • 🎨Customizable NFTs
    • Page 2
  • 🎁Lootbox On Chain
    • Page 3
  • Group 1
    • Page 4
  • SFTs: Flow and Smart Contracts
Powered by GitBook
On this page
  • The Game's General Flow
  • SFTs infrastructure
  • Smart Contracts
  • Burn function
  • Map-set
  • Map-get
  • The growth of the player into the game scenario
  • Buying Assets -> Acquisition
  • Crafting Assets -> Crafting
  • Upgrading Assets -> Level-Up
  • Rendering the User Interface to the Smart Contract Functionality
  • Set-Token-URI and Get-Token-URI

SFTs: Flow and Smart Contracts

PreviousPage 4

Last updated 2 years ago

The Game's General Flow

To highlight the benefits of implementing the SFT standard into Web3, we have built a scenario of an RPG mini game. The logic of the game is that the player will have to fight different mystic creatures in order to advance, but also he could farm in order to mint some of the resources. Minting new items will be possible using at least one of the following methods: acquisition, crafting and level-up. Providing functionality to each of these three methods will be captured inside a single smart contract using three different public functions.

SFTs infrastructure

General

Web3 games need to host their items, characters and all kinds of assets that are owned by the player. Here are some smart contracts designed for specific games as their dynamics and needs are different:

• MMO Strategy - resources ( gold, silver, wood ) and buildings ( town hall level 9, tower of fire level 3, storage level 5 )

• RPG - resources (gold, stamina, health) and items (wood sword level 2, steel armor plate level 4)

• Card Game - resources (gold) and cards (every single card is another type of SFT with its attack, health, mana cost and other stats)

Our Game

We have designed our game to contain SFTs divided into two categories: resources and items. We have considered resources the intermediaries to purchasing or crafting, while items can be equipped and used in order to advance into the game's plot. Each item has power attributes depending on the level and the base material which it is built of.

Smart Contracts

As we have mentioned before, Web3 gaming raises the need of handling the entire process in a decentralized, transparent and secure way. Creating the prerequisites for this fundamental need implies deploying smart contracts directly on the blockchain, and by this allowing the player to consult the smart contract / contracts which represent the basis of the game's functionality. Because of the fact that we know beforehand the whole flow of the game we are developing, we have only built a single smart contract which allows us to handle all the use cases. Next, we will breakdown the game's flow and the clarity functions we are using to operate the player's possible actions, described before.

Burn function

The clarity function that represents the ground for each of the three possible functionalities is the burn one. Its structure is the following:

(define-public (burn (token-id uint) (amount uint) (sender principal))
  (let((sender-balance (get-balance-uint token-id sender)))
    (asserts! (or (is-eq sender tx-sender) (is-eq sender contract-caller)) err-invalid-sender)
    (asserts! (<= amount sender-balance) err-insufficient-balance)
    (try! (ft-burn? semi-fungible-token amount sender))
    (try! (tag-nft-token-id {token-id: token-id, owner: sender}))
    (set-balance token-id (- sender-balance amount) sender)
    (print {type: "sft_burn", token-id: token-id, amount: amount, sender: sender})
    (ok true)))

(define-public (burn-wrapper (burn-tuple {resource-id: uint, resource-qty: uint})) 
  (burn (get resource-id burn-tuple) (get resource-qty burn-tuple) tx-sender))

The burn function is responsible for burning a given amount of a token from the sender address. Firstly, it creates a local variable called sender-balance which represents the balance of the certain token for the address which initiates the transfer. After that, it assures that the sender is valid and the amount of the sent token owned by the sender is enough to complete the burn. As it can be observed, it calls another function named tag-nft-token-id. It is structured like this:

(define-private (tag-nft-token-id (nft-token-id {token-id: uint, owner: principal}))
  (begin
    (and
      (is-some (nft-get-owner? semi-fungible-token-id nft-token-id))
      (try! (nft-burn? semi-fungible-token-id nft-token-id (get owner nft-token-id))))
    (nft-mint? semi-fungible-token-id nft-token-id (get owner nft-token-id))))

This function firstly checks if the owner has a certain type of token semi-fungible-token-id, and attempts to burn it for the given address. After that, it mints the token for the owner address. Going back to the transfer function, after handling the burn-mint phase, it updates the balances of the given token for each of the sender and the recipient. If none of the conditionals negatively influence the execution thread, the transfer function finally returns an (ok true) and prints a brief confirmation message of the transaction that has just been initiated and sent to the blockchain.

Map-set

map-set(we define them using a single SC bc we know beforehand, transparent for the players),

Here begins the stage where we establish the rules of the game. For every asset the player wants to obtain (by acquisition, crafting or level-up), he will need a series of required resources. These can be either resources or items. For every single token that needs to be minted, we will store the tokens required in exchange using a dictionary for each of the three possible operations. All of these dictionaries will be found inside one single smart contract which will be deployed on-chain. Here's an example:

(define-map  
  { id: uint } 
  (list 100 
    { resource-id: uint, resource-qty: uint }))
    
(map-set acquisition-system 
  {id: u5} 
  (list 
    {resource-id: u1, resource-qty: u15}))
    
(map-set acquisition-system 
  {id: u6} 
  (list 
    {resource-id: u1, resource-qty: u40} 
    {resource-id: u3, resource-qty: u7}))

(define-map  
  { id: uint } 
  (list 100 
    { resource-id: uint, resource-qty: uint }))
    
(map-set crafting-system 
  {id: u5} 
  (list 
    {resource-id: u3, resource-qty: u4}))
     
(map-set crafting-system 
  {id: u8} 
  (list 
    {resource-id: u4, resource-qty: u4}))

(define-map  
  { id: uint } 
  (list 100 
    { resource-id: uint, resource-qty: uint }))
    
(map-set level-up-system 
  {id: u6} 
  (list 
    {resource-id: u3, resource-qty: u6} 
    {resource-id: u5, resource-qty: u1} 
    {resource-id: u2, resource-qty: u2}))
    
(map-set level-up-system 
  {id: u7} 
  (list 
    {resource-id: u3, resource-qty: u10} 
    {resource-id: u6, resource-qty: u1} 
    {resource-id: u2, resource-qty: u3}))

Map-get

After on-chain storing in a transparent way the rules of the game, we will want to create queries onto the already set needed tokens in order to perform every single in-game operation. These queries will be executed using map-get method inside a read-only function, like that:

(define-read-only (get-acquisition-resources (token-id uint))
  (let ((token-urr (map-get? acquisition-system {id: token-id})))
    (ok token-urr)))

(define-read-only (get-crafting-resources (token-id uint))
  (let ((token-urr (map-get? crafting-system {id: token-id})))
    (ok token-urr)))

(define-read-only (get-level-up-resources (token-id uint))
  (let ((token-urr (map-get? level-up-system {id: token-id})))
    (ok token-urr)))

We have created a read-only function for each use case the player can fit in. Next, we will split the game's thread in three scenarios.

The growth of the player into the game scenario

As we have set before all the dictionaries containing the requirements for minting new assets, along with the read-only functions to create queries onto them, we will need three functions, one for each use case.

Buying Assets -> Acquisition

The Acquisition represents the rawest way a player can mint a new asset. By acquisitioning new items, the player will only burn resources, and not other owned items. For a balanced gameplay, only some of the items could be acquisitioned and the player will have to mix the other two use cases in order to successfully proceed. The function which will handle the Acquisition action looks like this:

(define-public (buy-item (id-new uint))
  (let ((acquisition-resources (unwrap-panic (get-acquisition-resources id-new)))
        (verified-ownership (fold and (map is-owned-needed (unwrap-panic acquisition-resources)) true)))
    (asserts! (is-some acquisition-resources) err-not-some)
    (asserts! verified-ownership err-insufficient-balance)
    (some (map burn-wrapper (unwrap-panic acquisition-resources)))
    (mint id-new u1 tx-sender)))

This function creates a local variable containing the resources needed for buying a new token and one for verifying if the tx-sender owns enough of the needed resources using is-owned-needed private function. After that, it ensures the token for which we perform the query has set resources inside the corresponding dictionary and the tx-sender owns enough of the needed tokens. After that it calls the burn-wrapper public function which is responsible for burning the given amount of the needed tokens from the sender.

(define-private (is-owned-needed  (item {resource-id: uint, resource-qty: uint}))
  (>= (get-balance-uint (get resource-id item) tx-sender) (get resource-qty item)))
(define-public (burn-wrapper (burn-tuple {resource-id: uint, resource-qty: uint})) 
  (burn (get resource-id burn-tuple) (get resource-qty burn-tuple) tx-sender))

Finally, if the function's execution thread is not interrupted by the previous conditionals, it mints one new token.

Crafting Assets -> Crafting

The Crafting represents the the way a player can construct a new asset. By crafting new items, the player will burn resources and items. The function which will handle the Crafting action looks like this:

(define-public (craft-item (id-new uint))
  (let ((crafting-resources (unwrap-panic (get-crafting-resources id-new)))
        (verified-ownership (fold and (map is-owned-needed (unwrap-panic crafting-resources)) true)))
    (asserts! (is-some crafting-resources) err-not-some)
    (asserts! verified-ownership err-insufficient-balance)
    (some (map burn-wrapper (unwrap-panic crafting-resources)))
    (mint id-new u1 tx-sender)))

This function follows the flow of the Acquisition one, because even if the in-game ways of obtaining a wanted token are very different, inside the smart contract they are almost the same.

Upgrading Assets -> Level-Up

The Level-Up represents the the way a player can upgrade an already owned asset. By upgrading existent items, the player will burn resources and old items in order to obtain a new token. The function which hadles the Level-Up action looks like this:

(define-public (level-up (id-new uint))
  (let ((level-up-resources (unwrap-panic (get-level-up-resources id-new)))
        (verified-ownership (fold and (map is-owned-needed (unwrap-panic level-up-resources)) true)))
    (asserts! (is-some level-up-resources) err-not-some)
    (asserts! verified-ownership err-insufficient-balance)
    (some (map burn-wrapper (unwrap-panic level-up-resources)))
    (mint id-new u1 tx-sender)))

This function follows the flow of the Acquisition and Crafting ones.

Rendering the User Interface to the Smart Contract Functionality

Set-Token-URI and Get-Token-URI

In order to enable the User Interface and render the assets images and attributes to the Front End we have to set, and then to be able to get one given token's URI. To allow this, we have created a token-uri dictionary. We will set for each token-id the corresponding JSON URL containing the token's attributes we have previously stored using a decentralized storage, in our case Pinata.

;; defining the dictionary
(define-map token-uri { id: uint } { url: (string-ascii 256) })

;; setting an URI for token-id 1
(map-set token-uri {id: u1} {url: "ipfs://QmcQzR4zcamVTzCPfCRBYywHVHGVncB2o3YpojvRmakVkC/1.png"})

;; defining the set public function
(define-public (set-token-uri (token-id uint) (token-url (string-ascii 256)))
  (begin 
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)    
    (map-set token-uri {id: token-id} {url: token-url}) 
    (ok true)))

;; defining the get read-only function
(define-read-only (get-token-uri (token-id uint))
    (let ((token-urr  (get url (map-get? token-uri {id: token-id}))))
      (ok token-urr)))