Cadence Language #art.1

Coding new-age (d)apps built on top of the Flow blockchain

Cadence Language #art.1

pre-note: to follow along this coding series you should have some prior knowledge of at least one mainstream programming language (such as javascript, python, java, c#, etc.)

Some context...

FLOW Blockchain

Flow is a fast, decentralized and developer-friendly blockchain, designed as the foundation for a new generation of games, apps, and the digital assets that power them. It is based on a unique architecture designed to scale without sharding. As many other blockchains, programs and business logics can be stored in user accounts as smart contracts. A smart contract is a program that verifies and executes the performance of a contract without the need for a trusted third party. Programs that run on blockchains are commonly referred to as smart contracts because they mediate important functionality (such as currency) without having to rely on a central authority (like a bank).

Cadence Language

Cadence is a resource-oriented programming language that introduces new features to smart contract programming that help developers ensure that their code is safe, secure, clear, and approachable. Some of these features are:

  • Type safety and a strong static type system
  • Resource-oriented programming, a new paradigm that pairs linear types with object capabilities to create a secure and declarative model for digital ownership
  • The utilization of capability-based security, which enforces access control by requiring that access to objects is restricted to only the owner and those who have a valid reference to the object

So let's start writing some code and digging in some of this features step by step, we will write a simple program to showcase some of the building blocks of the language architectural design patterns, taking for that a naive approach of a non-fungible token (NFT) creation, you will see how easy and natural it is in Cadence to ensure scarcity and acces control of data structures.

We start by defining the 'atomic' shape of our NFT, that's it, interfaces let you pre-define the attributes of your structs (kind of Classes in any OOP lang), mainly variables and methods. Structures are value types, so they are copied along the code, which means they can coexist and be used in multiple scenarios:

pub struct interface INFTAtom {
    pub let id: UInt8
    pub let author: String
    pub let hasOwner: Bool
    pub let meta: {String: String}?
    init(id: UInt8, author: String, hasOwner: Bool, meta: { String: String }? )
}

It's time to create the struct itself applying the interface shaper. Every struct has it's own initializer function, which can take any number of parameters to initialize its fields, like a constructor. Also note the '?' character chain in the public meta property, which denotes the optional declaration of this field:

pub struct NFTAtom: INFTAtom {
    pub let id: UInt8
    pub let author: String
    pub let hasOwner: Bool
    pub let meta: {String: String}?

    init(id: UInt8, author: String, hasOwner: Bool, meta: { String: String }? ) {
        self.id = id
        self.author = author
        self.hasOwner = hasOwner
        self.meta = meta
    }
}

Let's define now the 'shape' of two more data types: Resources in this case. These are 'linear types', instead of value types, instances that cannot be copied and only exists once by being moved around the code logic. For that, we will make use again of interfaces:

pub resource interface INFTMinter {
    pub let atom: NFTAtom
    init(atom: NFTAtom)
}
pub resource interface INFTWallet {
    pub let nft: @NFTMinter
    init(nft: @NFTMinter)
    pub fun logNFT()
}

We create two model shapes, first one will serve us to mint the NFT just one time to create the scarcity and give true value to the resource. The second one will represent our hypothetical wallet or vault where we will deposit our newly minted NFT, with a funcionality added to log the token content.

Next step is to create the actual resources:

pub resource NFTMinter: INFTMinter {
    pub let atom: NFTAtom
    init(atom: NFTAtom) {
        self.atom = atom
    }
}

pub resource NFTWallet: INFTWallet {
    pub let nft: @NFTMinter

    init(nft: @NFTMinter) {
        self.nft <- nft
    }
    pub fun logNFT() {
        log(self.nft.atom.id)
        log(self.nft.atom.author)
        if let meta = self.nft.atom.meta {
            log(meta["description"])
        } 
    }
    destroy() {
        destroy self.nft
    }
}

First declaration has nothing new to explain. The NFTWallet resource introduces some new concepts, in the initializer we associate the self property nft with the newly minted NFT using an arrow (<-) instead of an equal sign, which means that we are moving the resource NFTMinter to store it in the variable. Also, we use an if conditional statement in the logNFT function to check for the optional property meta.

Finally, we have to declare the function destroy to ensure the explicit lossness of the nft resource contained in the NFTWallet when the container is destroyed too, you can think of it in terms of a lifecycle hook. Without declaring that, the compiler won't let us compile the program so don't forget that or the terminal will prompt you with an error.

Now we have everything in place to execute our program. For that we will use the main function, which always represents the entry point of the application. First we instantiate our atomic NFT structure and store it in the variable atomX, then mint our NFT storing/moving it in a variable instantiating the NFTMinter resource, using for that the newly created atom. Lastly, we move it to our vault/wallet and we call the logNFT method to display some useful information. Finally we have to call the destroy method to remove the wallet resource and the NFT minted living inside it.

pub fun main() {
    let atomX: NFTAtom = NFTAtom(id: 0, author: "Shu Lea Cheang", hasOwner: false, meta: {
        "description": " Mixed media installation",
        "location": "Shanghai",
        "tags": "art, nft, abstract, cyber"
    })

    let nftMinted: @NFTMinter <- create NFTMinter(atom: atomX)
    let myWallet: @NFTWallet <- create NFTWallet(nft: <- nftMinted)
    myWallet.logNFT()
    destroy(myWallet)
}

The result of the program printed to the console should be:

// Console
0
"Shu Lea Cheang"
"Mixed media installation"

Complete code here:

pub struct interface INFTAtom {
    pub let id: UInt8
    pub let author: String
    pub let hasOwner: Bool
    pub let meta: {String: String}?
    init(id: UInt8, author: String, hasOwner: Bool, meta: { String: String }? )
}
pub resource interface INFTMinter {
    pub let atom: NFTAtom
    init(atom: NFTAtom)
}
pub resource interface INFTWallet {
    pub let nft: @NFTMinter
    init(nft: @NFTMinter)
    pub fun logNFT()
}

pub fun main() {
    let atomX: NFTAtom = NFTAtom(id: 0, author: "Shu Lea Cheang", hasOwner: false, meta: {
        "description": "Mixed media installation",
        "location": "Shanghai",
        "tags": "art, nft, abstract, cyber"
    })

    let nftMinted: @NFTMinter <- create NFTMinter(atom: atomX)
    let myWallet: @NFTWallet <- create NFTWallet(nft: <- nftMinted)
    myWallet.logNFT()
    destroy(myWallet)
}

pub struct NFTAtom: INFTAtom {
    pub let id: UInt8
    pub let author: String
    pub let hasOwner: Bool
    pub let meta: {String: String}?

    init(id: UInt8, author: String, hasOwner: Bool, meta: { String: String }? ) {
        self.id = id
        self.author = author
        self.hasOwner = hasOwner
        self.meta = meta
    }
}

pub resource NFTMinter: INFTMinter {
    pub let atom: NFTAtom
    init(atom: NFTAtom) {
        self.atom = atom
    }
}

pub resource NFTWallet: INFTWallet {
    pub let nft: @NFTMinter

    init(nft: @NFTMinter) {
        self.nft <- nft
    }
    pub fun logNFT() {
        log(self.nft.atom.id)
        log(self.nft.atom.author)
        if let meta = self.nft.atom.meta {
            log(meta["description"])
        } 
    }
    destroy() {
        destroy self.nft
    }
}