DEV Community

Cover image for Web3 tutorial: A notepad in the Flow blockchain
Juan Sagasti for The Agile Monkeys

Posted on • Updated on

Web3 tutorial: A notepad in the Flow blockchain

Introduction

While learning about web3 and blockchain technologies at The Agile Monkeys, we found out Flow. According to them, "Flow was designed for exceptional consumer and developer UX". And after playing with it for some time, I agree with that. Their developer UX is among the best, if not the best, we've tried. Some things that make Flow different are:

  • A greener web3 network. Flow starts with HotStuff, a proven Proof of Stake consensus mechanism, and adds a unique multi-node architecture to drive dramatic improvements in speed, throughput, and environmental friendliness without sharding or "layer two". This means Flow is the greenest web3 network among leading platforms.

Multi-node architecture

  • Resource-oriented programming with Cadence, their own programming language. If you are familiar with Swift, Rust, or TypeScript, you will love Cadence and will be writing Smart Contracts in no time.

  • Playgrounds. An online editor to write and test your Smart Contracts without needing to deploy them to the main or test networks. You can focus first on the business logic. This is what we will be using in this tutorial.

  • Upgradeable Smart Contracts. Securely and transparently patch bugs and upgrade pre-specified parts of a smart contract.

  • No Sharding. On Flow, every application can be a platform. Smart contracts and user accounts on Flow can always interact with each other in one atomic, consistent, isolated, and durable (ACID) transaction. In other words: all applications on Flow can run in the same shared execution state. This ensures Flow apps benefit from great user experience and full composability, letting developers easily build on each other’s work. Sharding and layer 2 solutions break composability and reduce network effects for dApps and smart contracts by removing ACID guarantees from the execution environment.

  • Client libraries to help you build your dApps, Smart User Accounts, and more!

Implementation

First of all, why will we build a notepad on the blockchain? That makes no sense! I know paying gas fees for creating notes wouldn't be an ideal use case. But the Internet is already crowded with NFTs tutorials, and I wanted a more classic approach for non-web3 devs that illustrates how Smart Contracts + the user account in Flow can act as the API and the backend of our hypothetical dApp. And hopefully, this will give you more ideas about other possible dApps!

Cadence is a resource-oriented programming language. This means it makes it easy to work with digital assets (resources) as if they were physical assets. A resource can only be in one place at a time. So, for example, if you assign (this is, move with the <- operator) a resource to a new variable, the resource won't be available in the original variable anymore, and that variable will be invalid:

let movedResource <- originalResource
Enter fullscreen mode Exit fullscreen mode

This makes the experience of working with digital assets pretty natural. A resource can't be left forgotten in a variable. It must live in your user account or should be destroyed at the end of the function you are currently executing. Otherwise, the compiler will complain about the resource loss. It's pretty safe in this regard.

I encourage you to read the quick Introduction to Cadence and the Cadence Language Reference before continuing this tutorial if you want to understand things better. Much of the language's syntax is inspired by Swift, Kotlin, and TypeScript, so you could feel at home if you are used to them.

Open a Flow Playground; you will see something like this:

Flow Playground

You'll find the test accounts, the transactions, and the script folders in the left sidebar. Transactions are mutations of the blockchain and require gas fees. Scripts are read-only, blazing-fast operations that allow you to perform queries without paying gas fees. Anyway, we don't need to worry about transaction fees in this tutorial.

The user's account filesystem domain structure can be seen as this:

Account Filesystem Domain Structure

According to Flow docs:

  • The storage domain is where all objects (such as structs and resource objects representing tokens or NFTs) are stored. It is only directly accessible by the owner of the account.
  • The `private domain is like a private API. You can optionally create links to any of your stored assets here. Only the owner and anyone they give access to can use these interfaces to call functions defined in their stored assets.
  • The public domain is your account's public API. It is like the private domain in that the owner can link capabilities here that point to stored assets. The difference is that anyone in the network can access the public domain's functionality. A Transaction in Flow is defined as an arbitrary-sized block of Cadence code that is signed by one or more accounts.

Transactions have access to the /storage/ and /private/ domains of the accounts that signed the transaction. Transactions can also read and call functions in public contracts and access public domains in other users' accounts.

I wanted to deploy a public contract in one user address and let the other users use it without needing to deploy the contract to every account that wants to use it. So let's start by selecting the 0x01 user account in the sidebar, removing the example code, and pasting our contract code. The code includes comments so you can understand the reasoning behind every part as part of this tutorial:
`

// There are different access levels, as defined in 
// https://docs.onflow.org/cadence/language/access-control/#gatsby-focus-wrapper, 
// but we want our contract to be accessible from outside this 
// account, so we defined it as public (pub). 
pub contract NotepadManagerV1 {
    // This var is just for fun, to know how many Notepads 
    // this contract created.
    pub var numberOfNotepadsCreated: UInt64

    // The Notepad resource. It contains the notes as nested 
    // resources in the 'notes' dictionary. 
    // It also defines the Notepad API.
    pub resource Notepad {

        // Dictionary of notes. We will use the note.uuid 
        // (UInt64) as the indexing key:
        pub var notes: @{UInt64 : Note}

        init() {
            // When a Notepad is created, we initialize the 
            // 'notes' property with an empty dictionary:
            self.notes <- {}
        }

        // Every Resource needs a 'destroy' method.
        destroy() {
            // As we have nested resources, we need to destroy 
            // them here too:
            destroy self.notes
        }

        // Public method to add a note to the user's Notepad.
        pub fun addNote(title: String, body: String) {
            // Create a new note Resource and move it to the 
            // constant:
            let note <- create Note(title: title, body: body)

            // As we are using a dictionary of resources and 
            // we are adding a new note with the note.uuid as 
            // the indexing key, Cadence requires you to 
            // handle the note that could already be in that 
            // dictionary's position to avoid a Resource loss.
            // So we move what could already be in that 
            // position to the 'oldNote' constant and destroy 
            // it. In this case, that won't do anything
            // because 'oldNote' will be nil (we are adding a 
            // new note to that position with a new UUID). 
            // And we move the new note to the new position. 
            // This chained move (<-) operators instruction 
            // could be read as: "We move the new note to that 
            // position, and what was previously in that 
            // position to a constant, so we don't lose it":
            let oldNote <- self.notes[note.uuid] <- note
            destroy oldNote
        }

        // Public method to edit a note in the Notepad.
        pub fun editNote(noteID: UInt64, newTitle: String, newBody: String) {
            // We create and move a new note to the previous 
            // note position, and the old note to the 
            // 'oldNote' constant (and then destroy it):
            let oldNote <- self.notes.insert(key: noteID, <- create Note(title: newTitle, body: newBody))
            destroy oldNote
        }

        // Public method to delete a note from the Notepad.
        pub fun deleteNote(noteID: UInt64) {
            // Move the desired note out from the dictionary 
            // (to a constant) and destroy it: 
            let note <- self.notes.remove(key: noteID)
            destroy note
        }

        // Public function that, given a note resource id in 
        // the Notepad, returns a NoteDTO from it (see this 
        // DTO definition below). 
        // This will be useful for client queries (Scripts), 
        // which are read-only, and we cannot just return a 
        // Resource there. 
        pub fun note(noteID: UInt64): NoteDTO {
            // We take the note out of the notes dictionary, 
            // create a NoteDTO from it, put the note Resource 
            // back into the dictionary, and return the NoteDTO:
            var note <- self.notes.remove(key: noteID)!
            let noteDTO = NoteDTO(id: noteID, title: note.title, body: note.body)

            let oldNote <- self.notes[noteID] <- note
            destroy oldNote
            return noteDTO
        }

        // Public function that returns an array of NoteDTO 
        // (see this DTO definition below). This will be 
        // useful for client queries (Scripts), which are 
        // read-only, and we cannot just return a Resource. 
        pub fun allNotes(): [NoteDTO] {
            // We get a NoteDTO array from the note Resources 
            // in the Notepad. 
            var allNotes: [NoteDTO] = []
            for key in self.notes.keys {
                allNotes.append(self.note(noteID: key))
            }

            return allNotes
        }
    }

    // Note Resource definition. This is a nested Resource inside the Notepad Resource.
    pub resource Note {
        pub(set) var title: String
        pub(set) var body: String

        init(title: String, body: String) {
            self.title = title 
            self.body = body
        }
    }

    // Helper DTO (Data Transfer Object) that will be used 
    // when returning notes data to the clients from queries 
    // (Scripts). We cannot just return Resources there.
    pub struct NoteDTO {
        pub let id: UInt64
        pub let title: String
        pub let body: String

        init(id: UInt64, title: String, body: String) {
            self.id = id
            self.title = title 
            self.body = body
        }
    }

    init() {
        // Right after the contract is deployed, we initialize 
        // this var to keep the count of the Notepads that 
        // were created using this contract: 
        self.numberOfNotepadsCreated = 0
    }

    // Public method to create and return a Notepad Resource.
    pub fun createNotepad(): @Notepad {
        self.numberOfNotepadsCreated = self.numberOfNotepadsCreated + 1
        return <- create Notepad()
    }

    // Public method to destroy a Notepad resource from the 
    // user's Account.
    pub fun deleteNotepad(notepad: @Notepad) {
        self.numberOfNotepadsCreated = self.numberOfNotepadsCreated > 0 ? self.numberOfNotepadsCreated - 1 : 0
        destroy notepad
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, let's deploy the contract by tapping on the Deploy or Redeploy button at the top-right corner of the Playground:
Deploy the Smart Contract

Once the contract is deployed to the 0x01 account, you will see it in the sidebar:

Deployed Contract

Now it's time to test our contract using Transactions and Scripts. Let's start by adding a new transaction to the sidebar. This transaction will create the needed notepad and test the API by adding, editing, and deleting notes:

// Import the Smart Contract from the user account it was deployed to:
import NotepadManagerV1 from 0x01

transaction {

 // In the prepare function, we have access to the private 
 // space of the user's account who's signing the transaction:
    prepare(acct: AuthAccount) {
        // We try to borrow the notepad from the storage place 
        // where it is stored. Borrowing here means that we 
        // are creating a reference to the notepad without 
        // actually moving it to a var from its location:
        var notepad = acct.borrow<&NotepadManagerV1.Notepad>(from: /storage/Notepad)

        // If the result of that borrow is nil, that means we 
        // didn't store a notepad yet there, so we do it:
        if notepad == nil {
            // Create a notepad using our contract and move 
            // the returned resource to the /storage/Notepad 
            // path:
            acct.save(<- NotepadManagerV1.createNotepad(), to: /storage/Notepad)

            // Make the notepad public by creating a public 
            // link stored in /public/PublicNotepad. We are 
            // making it public because we need to expose it 
            // if we want to query and return that info from a 
            // Script later. These 'link' and 'borrow' 
            // functions are related to Capability-based 
            // Access Control, which is a pretty powerful 
            // feature that lets you expose a public API over 
            // the fields and functions of your stored assets 
            // to other users. If you want to dig down deeper 
            // on the topic, please refer to
            // https://docs.onflow.org/cadence/language/capability-based-access-control/:
            acct.link<&NotepadManagerV1.Notepad>(/public/PublicNotepad, target: /storage/Notepad)
            log("Notepad created!")
        }

        // We borrow the notepad (in this case we are sure we 
        // have created it because of the previous code):
        let theNotepad = acct.borrow<&NotepadManagerV1.Notepad>(from: /storage/Notepad)

        // Test note creation:
        theNotepad?.addNote(title: "First note title", body: "This is the body of the first note.")
        log("The first note was created and added to the notepad!")

        // Test another note creation:
        theNotepad?.addNote(title: "Second note title", body: "This is the body of the second note.")
        log("The second note was created and added to the notepad!")

        // Test note edition by editing the second note:
        theNotepad?.editNote(noteID: 2, newTitle: "Edited second note title", newBody: "EDITED: This is the body of the second note.")
        log("The second note was edited!")

        // Test note deletion by deleting the first note:
        theNotepad?.deleteNote(noteID: 1)
        log("The first note was deleted!")
    }

    execute {
        // We don't need to execute anything else for our 
        // testing. You can even delete this method. 
        log("Transaction executed!")
    }
}
Enter fullscreen mode Exit fullscreen mode

To execute this transaction, tap on the Send button in the top-right corner of the Playground, but make sure you add the 0x02 account as the signer, so you test the transaction from a different account from where the contract is located (0x01):
Send Transaction

After sending it, it will be executed, and you will see the logs in the Transaction Results console:
Transaction Results

If you enable the Resources Explorer by tapping on the database icon next to the 0x02 account in the sidebar, you will see an empty storage panel above the Transaction Result console:

Resources Explorer

The notepad containing the notes should appear there. This is a recent bug in the Playground they haven't fixed yet. It was working in previous versions. But don't worry, we can check the content with a script.

So let's create a new script in the sidebar and paste this code:

import NotepadManagerV1 from 0x01

// A script is a special type of Cadence transaction
// that does not have access to any account's storage
// and cannot modify state. Any state changes that it would
// do are reverted after execution.
//
// Scripts must have the following signature: pub fun main()
// We can specify a return type, in this case we are returning 
// an array of NoteDTO:
pub fun main(): [NotepadManagerV1.NoteDTO]? {

    // Cadence code can get an account's public account object
    // by using the getAccount() built-in function.
    let notepadAccount = getAccount(0x02)

    // Get the public capability from the public path of the 
    // owner's account.
    // Remember that we made this public in the test 
    // transaction. 
    let notepadCapability = notepadAccount.getCapability<&NotepadManagerV1.Notepad>(/public/PublicNotepad)

    // Borrow a reference for the capability
    let notepadReference = notepadCapability.borrow()

    // If the notepad doesn't exist yet publicly, we return nil. 
    // Otherwise, we return all the notes in the form of an array of NoteDTO
    return notepadReference == nil ? nil : notepadReference?.allNotes()
}
Enter fullscreen mode Exit fullscreen mode

Execute the script by tapping on the Execute button on the top-right side of the Playground, and you will see the result printed in the console. The notepad containing the edited note:

20:57:37 
Query notes 
Result
{"type":"Optional","value":{"type":"Array","value":[{"type":"Struct","value":{"id":"A.0000000000000001.NotepadManager.NoteDTO","fields":[{"name":"id","value":{"type":"UInt64","value":"2"}},{"name":"title","value":{"type":"String","value":"Edited second note title"}},{"name":"body","value":{"type":"String","value":"EDITED: This is the body of the second note."}}]}}]}}
Enter fullscreen mode Exit fullscreen mode

Finally, if you want to easily reset all data stored in the Playground accounts, you can do it by redeploying the contract.

Link to the full Playground

That is enough for now! I hope you have enjoyed playing with Flow. In the next article, we'll learn to deploy the contract to the actual testnet, create a native iOS dApp that leverages all of this, and communicates directly with the Flow's network. Link to the second part:

How to build a native iOS dApp that uses the Flow Blockchain as the backend

Thanks for reading!

Oldest comments (1)

Collapse
 
muttoni profile image
Andrea Muttoni

This is awesome! Thanks so much for sharing