Interfaces on Flow

Interfaces on Flow

Β·

15 min read

Hi! πŸ‘‹ My name is Phill and I am Blockchain Tech Lead at The Fabricant and a Community Rep for Flow. You can find me on:
Discord: Phill#1854
Email:
Github: github.com/ph0ph0
Twitter: twitter.com/fullstackpho

For a curated collection of the best Flow tools, blogs and articles, check out Get The Flow Down!


πŸ“– Introduction

Interfaces are a common software development paradigm found in many different programming languages. In this article, we will introduce interfaces as a development tool, so that we can explore them further in the context of the Flow blockchain. By doing so, we aim to shed light on their importance, and will hopefully give you the confidence to design and integrate your own interfaces into your next Cadence project!

The two main topics covered in this article are:

  • An introduction to interfaces in software and interfaces on Flow.

  • How to use interfaces on Flow and why they are powerful.

Let's get to it!

πŸ‘¨β€πŸ’» Interfaces in Software

Interfaces are an important tool in software development, acting as an agreement between two components that outlines how they should interact.

Interfaces define methods, attributes, and events without specifying implementation details, allowing developers to focus on functionality and behaviour.

They offer two main benefits: modularity and reusability. Modularity enables developers to break down complex systems into smaller, manageable parts, therefore making code more maintainable. Reusability lets different components share the same interface, making it easy to swap or replace them without affecting the system's overall operation.

🌊Interfaces on Flow

On Flow, an interface is an abstract type that defines the behaviour of implementing types by specifying required functions, fields, access control, preconditions and postconditions.

There are three kinds of interfaces: structure interfaces, resource interfaces, and contract interfaces.

To define an interface, you simply state the composite type that it applies to, followed by the interface keyword and then the name of your interface:

pub contract interface MyContractInterface {}
pub resource interface MyResourceInterface {}
pub struct interface MyStructInterface {}

Composite types that implement interfaces follow nominal typing, meaning a type only implements an interface if it explicitly declares conformance. In other words, it must state that it employs the interface on the line that declares the composite type. For a contract that inherits from the MyContractInterface above, this would look like this:

pub contract MyContract: MyContractInterface {}

Simple!

πŸ™‹β€β™‚οΈ Why, Where and How are Interfaces Used on Flow?

By now, I'm sure you will understand that an interface creates an 'agreement' between the composite type and the specified interface. It states that the functions, fields, access control, preconditions and postconditions declared in the interface should be implemented in the composite type. So why would we do this on Flow?

Interfaces provide a common blueprint for multiple components with similar functionality. However, one of the most powerful applications of interfaces is that they can be used in types as 'restricted types'.

A restricted type is a composite type that implements an interface or a group of interfaces. The restricted type is specified using the syntax {MyInterface} anywhere a type can be specified eg function parameter, field type etc.

By using restricted types in our code, we know that the composite type that we are handling implements a defined set of behaviours/properties.

For example, the Non-Fungible Token and Fungible Token standards on Flow are both interfaces, and by using their associated restricted types, we can predictably interact with any composite type that employs them. For example, consider the hypothetical NonFungibleToken interface below and the contracts that follow:

// HYPOTHETICAL NonFungibleToken interface 
pub resource interface NonFungibleToken {
    pub let id: UInt64

    pub fun getId():UInt64 {
        return self.id
    }
}

// First NFT contract
pub contract MyFirstNFT {

    // NFT resource employs the NonFungibleToken resource interface
    pub resource NFT: NonFungibleToken {

        // `id` must be included, as defined by the interface
        pub let id: UInt64

        // `getId()` must be implemented according to the interface
        pub fun getId(): UInt64 {
            return self.id
        }

        init(id: UInt64) {
            self.id = id
        }
    }
}

// Second NFT contract
pub contract MySecondNFT {

    // NFT resource employs the NonFungibleToken resource interface
    pub resource NFT: NonFungibleToken {
        // `id` must be included, as defined by the interface
        pub let id: UInt64

        // `getId()` must be implemented according to the interface
        pub fun getId(): UInt64 {
            return self.id
        }

        init(id: UInt64) {
            self.id = id
        }
    }
}

pub contract NFTManager {
    // Both of the contracts, NFT and MySecondNFT implement the `getId` function and inherit from the NonFungibleToken resource interface. Therefore, we can pass in a reference to the `NonFungibleToken` restricted type and we know that it will implement the `getId()` function.
    pub fun getNFTId(nft: &{NonFungibleToken}): UInt64 {
        return nft.getId()
    }
}

In the example above, we have defined a NonFungibleToken interface, followed by two NFT contracts that contain NFT resources that employ this interface.

In the fourth contract, NFTManager, we have the function getNFTId(nft: &{NonFungibleToken}): UInt64 . This function takes a restricted type of &{NonFungibleToken}, which represents a reference to a resource type that employs the {NonFungibleToken} interface.

Since the resource behind the reference implements the {NonFungibleToken} interface, we can be certain that it defines a getId() method. Therefore, any nft argument that is successfully passed into the getNFTId() function will have implemented the getId() function, so we can call this method in the body of our function!

This demonstrates the use of interfaces and how they can promote standardisation and predictability within and across code bases!

πŸ”‹ The Power of Interfaces

Another great example of the power of interfaces can be found in the MetadataViews standard that is utilised extensively throughout the Flow NFT ecosystem. The contract defines the resource interface, Resolver:

// Abridged vesion of MetadataViews    
pub contract MetadataViews {    

    /// Provides access to a set of metadata views. A struct or 
    /// resource (e.g. an NFT) can implement this interface to provide access to the views that it supports.
    ///
    pub resource interface Resolver {
        pub fun getViews(): [Type]
        pub fun resolveView(_ view: Type): AnyStruct?
    }

    ...

    pub struct Display {
        pub let name: String
        pub let description: String
        pub let thumbnail: AnyStruct{File}

        init(
            name: String,
            description: String,
            thumbnail: AnyStruct{File}
        ) {
            self.name = name
            self.description = description
            self.thumbnail = thumbnail
        }
    }
...
}

The resource interface above, Resolver, is employed by NFT resources and allows them to inform the caller about the 'views' that they implement. A view is a set of properties that provide information about the NFT. For example, the Display view above is one of the most basic views and it contains the name , description and thumbnail that are associated with this NFT.

The struct types (views) play an important role in the standardisation of metadata for Flow NFTs. They group properties according to their intended usage. For example, there is another view called Royalty that is used to inform the caller about the royalties that should be distributed when the NFT is sold on a marketplace.

Though it may not be immediately obvious, this is incredibly powerful. If all NFTs on Flow follow this convention, it means that all services that aggregate NFTs (eg marketplaces etc) can easily display any Flow NFT on their platform! There is no need for the developers of the aggregator to consult every individual NFT contract to work out which fields represent the NFT name, description, thumbnail, etc, because this simple approach provides a standard for gathering this information in an incredibly efficient manner πŸ’ͺ

πŸ›  Using Interfaces In Our Code

Interfaces can be used anywhere that you require composite types to have predictable properties and behaviours.

For example, let's imagine that we are designing an NFT contract where the NFTs themselves are Creatable - in other words, the user selects from a set of characteristics that will be combined and incorporated into the NFT metadata, creating an NFT composed of unique 'characteristics'.

In this example, there are a set of characteristics, and a Characteristic represents an attribute that a user can select from this set. For example, if we had a set of characteristics called hairColour, then selectable Characteristics might be brown, blonde etc.

In this example, we want to restrict our NFTs to unique combinations - there can only be one combination of Characteristics that should exist. We should make it easy for anyone to pull down information about the combinations, and it should be generic enough that we are not tied into a defined number of groups of characteristics or types (eg one NFT contract may allow users to select the number of arms, number of legs, hair colour, while another contract can allow engine size, horsepower etc).

Given the specification above, we could come up with something like this:

pub contract interface Creatable {
    // Dictionary that maps the combination of characteristics to the NFT id.
    // String represents the combination string, which is a concatenation of the ids of the characteristics.
    // UInt64 is the id of the NFT that possesses this combination
    access(contract) var combinations: {String: UInt64}

    // Get all the combinations that have been minted
    pub fun getCombinations(): {String: UInt64}

    // Interface for NFT
    pub resource interface CreatableNFT {

        // Function to get the characteristics that make up an NFT.
        // Returns an array of Characteritics
          pub fun getCharacteristics(): [AnyStruct{Creatable.Characteristic}]
    }

    pub struct interface Characteristic {
        pub let id: UInt64
        pub let type: String
        pub let name: String
        pub let description: String
        pub let value: AnyStruct
    }
}

In the contract interface above:

  • We define a dictionary called combinations. This is used to keep track of the combinations that have been minted. This can therefore be used to discover which combinations have been minted (via getCombinations()), and also to check during minting that the selected combination hasn't yet been minted, and therefore the NFT is unique in that respect.

  • The CreatableNFT resource interface definition is intentionally sparse to provide the developer with the freedom to design their NFT in their own way. We do not tell the developer how they should store their characteristics, only that they should be retrievable via the getCharacteristics() method which is used to inform the caller about the characteristics that were combined to make this particular NFT.

  • Finally, we have the Characteristic struct interface. This simply defines the properties that should be contained within a characteristic struct.

Let's look at how we could implement this in our NFT contract:

// Import paths below are placeholders 
import NonFungibleToken from <NonFungibleTokenPath>
import Creatable from <CreatablePath>

// In the interest of brevity, the NonFungibleToken standard is not fully implemented in this contract, nor are events, paths etc
pub contract AnimalNFT: NonFungibleToken, Creatable {

    pub var totalSupply: UInt64 
    access(contract) var combinations: {String: UInt64}

    access(contract) var furColours: {UInt64: {Creatable.Characteristic}}
    access(contract) var appendages: {UInt64: {Creatable.Characteristic}}

    init() {
        self.totalSupply = 1
        self.combinations = {}

        self.furColours = {
          1: AnimalNFT.Characteristic(
            id: 1,
            type: "furColour",
            name: "Fur Colour",
            description: "Clothed in a panoply of vibrant hues, such creatures dwell in the realm where ethereal rainbows unfurl their luminescent bands.",
            value: "Rainbow"
          ),
          2: AnimalNFT.Characteristic(
            id: 1,
            type: "furColour",
            name: "Fur Colour",
            description: "Swathed in an ever-changing sheen, creatures of metallic fur shimmer like living sculptures.",
            value: "Metallic"
          )
        }

        self.appendages = {
          1: AnimalNFT.Characteristic(
            id: 1,
            type: "appendages",
            name: "Appendages",
            description: "Endowed with intricate helical appendages, creatures of this ilk embody the beautiful mystery of nature's infinite creativity.",
            value: "Helical"
          ),
          2: AnimalNFT.Characteristic(
            id: 1,
            type: "appendages",
            name: "Appendages",
            description: "Brandishing appendages akin to fractal foliage, these remarkable creatures pulse with a harmonic rhythm.",
            value: "Foliage"
          )
        }
    }    

    pub fun getCombinations(): {String: UInt64} {
        return self.combinations
    }

    pub struct Characteristic: Creatable.Characteristic {
        pub let id: UInt64
        pub let type: String
        pub let name: String
        pub let description: String
        pub let value: AnyStruct

        init(
          id: UInt64,
          type: String,
          name: String,
          description: String,
          value: AnyStruct
        ) {
          self.id = id
          self.type = type
          self.name = name
          self.description = description
          self.value = value
        }
    }

    pub resource NFT: Creatable.CreatableNFT {

      pub let id: UInt64

      pub var characteristics: [{Creatable.Characteristic}]

      init(
        furColourId: UInt64, 
        appendageId: UInt64
      ) {
        self.id = AnimalNFT.totalSupply
        AnimalNFT.totalSupply = AnimalNFT.totalSupply + 1
        let furColour = AnimalNFT.furColours[furColourId] ?? panic("No fur colour exists with this id")
        let appendages = AnimalNFT.appendages[appendageId] ?? panic("There are no appendages with this id")
        self.characteristics = [furColour, appendages]

       AnimalNFT.combinations[AnimalNFT.createComboString(furColourId: furColourId, appendageId: appendageId)] = self.id
      }

        // Function to get the characteristics that make up an NFT.
        // Characteristic is the struct below 
        pub fun getCharacteristics(): [{Creatable.Characteristic}] {
          return self.characteristics
        }
    }

    pub fun mintNFT(furColourId: UInt64, appendageId: UInt64): @NFT {
      pre {
        !AnimalNFT
          .combinations
          .containsKey(AnimalNFT.createComboString(furColourId: furColourId, appendageId: appendageId)): "Combination has already been minted"
      }

      return <- create NFT(
        furColourId: furColourId,
        appendageId: appendageId
      )
    }

    access(contract) fun createComboString(furColourId: UInt64, appendageId: UInt64): String {
      return "furColour"
          .concat(furColourId.toString())
          .concat("_")
          .concat("appendages")
          .concat(appendageId.toString())
    }
}

There is quite a lot going on in the contract above so we will take it step-by-step.

  • After importing the NonFungibleToken standard (note that this is a version that was simplified as it is not the focus of this topic) and the Creatable interface, we state that our contract will inherit from these interfaces on the line pub contract AnimalNFT: NonFungibleToken, Creatable.

  • Next, we define the totalSupply, which comes from the NFT standard, and the combinations dictionary. This is used to keep track of the Characteristic combinations that were minted to prevent duplicates, but can also be used by dApps to query the combinations that have been minted and their assoicated nft IDs.

  • furColours and appendages are used to hold the data that makes up the set of Characteristics from which a user can choose. They are both dictionaries, where each Characteristic value has an associated id, which is used as the key. In this example contract, the values are set in the contract's init function, but they could easily be set by an Admin resource controlled setter function.

  • The init function of the contract sets the initial values for the contract's properties. At the top, we set the totalSupply and the combinations. Next, we populate the furColours and appendages dictionaries with the values that the user can select from.

  • getCombinations() returns the combinations that have been minted using the combinations dictionary.

  • The Characteristic struct inherits from the Characteristic struct interface in the Creatable interface. It simply defines the properties that are needed for it to follow the Characteristic interface.

  • Next, we have the NFT resource. This inherits from Creatable.CreatableNFT. As you can see, it is quite simple - it defines the id of the NFT and the characteristics array, as indicated by the interface. In the init function of this resource, we begin by setting the NFT id. Then we take the ids of the characteristics from the parameters of the init function and we assign these to the characteristics array. Notice how we grab the entire characteristic from the furColours and appendages dictionaries, which we then package into the array. At the bottom of the init function, we assign the combination that was minted to the nft's ID in the combinations dictionary. We use a helper function called createComboString to do this, which is described below.

  • getCharacteristics is a function that returns the characteristics that make up this NFT.

  • The mint function is where we create our NFT. It takes the ids of the characteristics and firstly checks whether or not this particular combination has been minted yet. This is done in the precondition block (pre {}). Once we are sure that our combination is available, we create our NFT.

  • Finally, we have the createComboString helper method. This takes the ids of the characteristics and combines them into a 'combination string` which we use as a proxy for identifying combinations.

Through the use of interfaces, we can communicate the expected behaviours and properties that are expected from composite types that inherit them. The AnimalNFT contract is an example of this, where the Creatable interface indicates that child NFT contracts should implement the functions, variables and constants that allow users to combine characteristics for their NFTs.

A playground that demonstrates the Creatable interface applied to the AnimalNFT contract can be found here.

The interface acts as a template ensuring predictability amongst its child contracts. This makes interacting with assets generated from these contracts a lot simpler, because we know, for example, that all their NFTs will implement getCharacteristics, and we also know the return type of this function. Any code that interacts with these NFTs, whether it is contract code, frontends or backends, will not have to know or understand the implementation details of the inheriting contract itself. This makes the interactions a lot more generic, and prevents the need for the end-users to write code specific to each new NFT contract. For example, alongside our AnimalNFT contract, we could create many different contracts that all inherit from the Creatable interface - CarNFT, HouseNFT, WeaponNFT - since they all follow the same pattern defined in the interface, we won't need to write code specific to each contract when interacting with their assets!

🎣 Interfaces and Contract Borrowing

A really cool feature of Cadence is contract borrowing. In scripts and transactions, contracts can be 'borrowed', effectively allowing them to be used as dynamic imports.

Typically when we write our scripts and transactions, we begin them with import statements at the top of the file, where we specify the contracts that we would like to use within the interaction eg:

import Creatable from "./contracts/Creatable.cdc"

By combining interfaces with the ability to borrow contracts, we can make our interactions even more generic.

For a contract to be borrowed, it must implement at least one interface, and when we borrow this contract, we must specify the interface that we are restricting the contract by (remember restricted types?). The borrowed contract type in our interaction will then have access to the public properties and functions that were defined in the interface.

This makes building on top of our smart contracts and interfaces even easier, because we no longer need to explicitly state the contract that we would like to import into our code, only the interface. Below is an example of a script using the Creatable interface:

import Creatable from <CreatableImportPath>

pub fun main(address: Address, contractName: String): {String: UInt64} {
    let contractAccount = getAccount(address)
    let creatableContract = contractAccount.account.contracts.borrow<&Creatable>(name: contractName)
    return creatableContract.getCombinations()
}

Using the script above, we can call the getCombinations method from Creatable on any contract that implements this interface - all we need to pass in is the address that the contract is deployed to and the name of the contract!

🌯 Wrapping up

Interfaces in software development act as agreements between components, enhancing modularity and reusability. In Flow, interfaces define behaviour for composite types, promoting code consistency. The Cadence feature of contract borrowing, combined with interfaces, enables more generic interactions. Interfaces form the backbone of efficient systems in blockchain and smart contract development - so think about how you can include these effectively in your code!

Further work:

The Characteristic struct contains the info we need to display the chars on our platform.

Take this further and create your own views.

The Chars array in the NFT means all NFTs will have this property.

The getter functions provide easy access to the info.

Playground link with tx

Find more info here.

Template txs

Contract interfaces and struct interfaces.

How can we create our own and why and use that?

Β