⚠️ This guidance is in prep for a Playground tutorial, but attachments must first be enabled on Playground for this guidance to make sense.
To showcase how attachments unlock permissionless composability on Flow, this tutorial will walk you though making your own cat-inspired NFT collection and a wardrobe of hats to go along with them. Let's put some hats on some cats!
Before digging in, let's talk about what we mean by composability.
Composability is the ability for existing components in one construction to be reused and even extended for another
purpose.
Think of composable architectures like building with legos
- you build something you need as a block or set of blocks and someone else can come along and use those same blocks to
build something entirely new.
Composability on Flow, especially with attachments, is permissionless because not only can you build with existing components, but no one can stop you from doing so! Hence the term permissionless!
Permissionless composability is one of the unique characteristics of decentralized app development because, as builders, we can freely compose richer or derivative functionalities on top of any existing contracts from other applications or communities. Contracts that you deploy which interact with other contracts are not subject to internal changes or control by the contract you composed it upon.
Cadence is uniquely suited for composable architectures because Cadence contracts can compose other contracts. As a developer, this means that working with contracts is very similar to working with libraries in other languages. Simply import them into your contract, scripts, or transactions, and they will be loaded dynamically on-demand for you to leverage their types and functionalities.
Less duplicate code, easier to bootstrap attention and adoption, and users benefit from the utility you built into something they already presumably own and love - win-win-win!
- Building on the existing standard
- Encouraged to also implement MetadataViews and ViewResolver contract interfaces, but keeping things simple
- We're defining the KittyVerse NFT for the purposes of this tutorial, but just as easily could've used an existing NFT project since we're building with attachments
- If totally new to this, take a look at NonFungibleToken tutorials - TODO: Link these
- Contract interface implementation
- Events
- Nested resource definitions
- Attribute overview
- Opportunities to extend the contract
- Deploy NonFungibleToken to
0x01
- Deploy KittyVerse to
0x02
- send
tx_01_setup_kittyverse_account.cdc
signing with0x02
- send
tx_02_mint_kittyverse_nft.cdc
signing with0x02
- send
tx_02_mint_kittyverse_nft.cdc
again signing with0x02
- query NFT.id - execute
script_01_get_kittyverse_ids.cdc
providing0x02
as argument- see two unique NFT IDS - take note of them, next let's prep them some hats
- What are they?
- Why do they matter?
- Can create attachments for anything without needing original developer's:
- Participation
- Permission
- Can create attachments for anything without needing original developer's:
- Conceptual
- Attachments are a special type that can be added to resources (doc link needed)
- You might be tempted to simply define all your functionality in the attachment, but it's important to know that
attachments are not resources.
- You'll lose the existence and uniqueness guarantees inherent to resources as well as make access control significantly harder for yourself
- As mentioned earlier, attachments are best thought of as glue between resources
- Define an attachment that can hold some resource, designed to be added to a base resource
- Note that you need access to the base resource to add an attachment to it
- So to add an attachment to an NFT, you need to first withdraw that NFT from its Collection
- Syntax
-
Accessing an attachment
- In the general sense, accessing an attachment looks similar to indexing a dictionary on a static type declaration where the return type is an optional reference to the attachment type
let attachmentRef: &A? = baseResource[A]
- In the case of KittyVerse and KittyHats, you'll see the following:
let attachmentRef: &KittyHats.HatAttachment? = nft[KittyHats.HatAttachment]
-
Adding an attachment
- Note that unlike resources, attachments can be created anywhere, including outside of the scope of the
contract that defines them
- This is an important consideration for how you design your attachments - you do not have the initialization
control that you do with resources
- Attachments can only be created while being attached to a base resource - attachments can only be created
in the context of an
attach
expression
- Attachments can only be created while being attached to a base resource - attachments can only be created
in the context of an
- This is an important consideration for how you design your attachments - you do not have the initialization
control that you do with resources
- Let's take a look at the general case of adding an attachment to a base resource. Adding an attachment
requires us to reassign the base resource to the result of the
attach
keyword
let resourceWithAttachment <- attach A() to <- baseResource
- And in the case of KittyVerse and KittyHats, you'll see the following helper method that adds a HatAttachment to a KittyVerse NFT:
access(self) fun addAttachment(toNFT: @KittyVerse.NFT): @KittyVerse.NFT { // If attachment already exists, return if toNFT[HatAttachment] != nil { return <-toNFT } // Otherwise, add the attachment return <- attach HatAttachment() to <- toNFT } // Otherwise, add the attachment return <- attach HatAttachment() to <- toNFT
- Note that unlike resources, attachments can be created anywhere, including outside of the scope of the
contract that defines them
-
- Why are we defining attachments instead of simply adding a field on the KittyVerse NFTs that can store hats?
- If we didn't define KittyVerse, we couldn't do that - no way we can alter the underlying contract
-
Attachments allow us to permissionlessly extend existing resources with new data and functionality
-
Why would we do that anyway?
- If KittyVerse already has an enthusiastic community behind it
-
- Let's say we're defining both, why use attachments?
- Building with attachments is a clear signal that you're designing for this permissionless composability
- You encourage others to define their own sorts of attachments for KittyVerse NFTs
- Building with attachments is a clear signal that you're designing for this permissionless composability
- If we didn't define KittyVerse, we couldn't do that - no way we can alter the underlying contract
- Definitions
- Common misconception: The attachment is the thing
- Correction: Attachments glue the thing(s)
- As mentioned previously, attachments are like glue between resources
- As of now, attachments can be fully referenced anywhere the NFT can be referenced - this can complicate access
control
- We made
removeHat()
access(contract)
and callable in Collection only- Link to access modifier docs here: https://developers.flow.com/cadence/language/access-control
- If we didn't, anyone would have been able to remove a KittyHats NFT from via a user's Collection public Capability
- We made
- Correction: Attachments glue the thing(s)
- Basically enabling the base resource - KittyHats NFT in this case - to receive a KittyHats NFT
- Your standard Collection
- Added ability to remove KittyHats from KittyVerse NFTs and return them to their Collection
- Deploy KittyHats to
0x03
- send
tx_03_setup_kittyhats_account.cdc
signing with0x02
- query NFT.id -
script_03_get_kittyhats_ids_and_names.cdc
providing0x02
as argument- see one id and note it for the next step
- send
tx_05_attach_hat_to_cat.cdc
signing with0x02
- provid KittyHats.NFT.id & KittyVerse.NFT.id of your choice
- query cats & hats -
script_04_get_kittyverse_names_and_hat
providing0x02
as argument- see the KittyVerse.NFT names - one should have your hat name and the other should have nil - Your cat has a hat!
- query KittyHats NFT IDs -
script_03_get_kittyhats_ids_and_names.cdc
providing0x02
as argument- see that your hat is no longer in your Collection
- send
tx_06_remove_hat_from_cat.cdc
signing with0x02
and providing your KittyVerse NFT id name - query cats & hats -
script_04_get_kittyverse_names_and_hat
providing0x02
as argument- see that your cat's hat has been removed
- query KittyHats NFT IDs -
script_03_get_kittyhats_ids_and_names.cdc
providing0x02
as argument- see that your hat is back in your Collection
- Great for
- Building on existing resources uilding in other contracts
- Establishing a pattern that encourages others to build on those resources you're creating
- Gotchas: There are some things to bear in mind when working about attachments
- Accessing attachments of a given type requires a static type declaration
- Attempting to add an attachment to a resource when one already exists will result in a runtime error
- This is why we check if an attachment already exists before adding one in the example above
- You'll want to lean on simple helper methods to conditionally add attachments within your contracts
- Since attachments are native to the language, there is no way to avoid the emergence of attachments on the things you deploy to the wild
- Implement MetadataViews and ViewResolver interfaces
- Simple implementation
- Add image URIs to the NFTs
- Add Admin resources to both contracts to customize
- Enable addition/removal of possible cats & hats + greetings
- Private minting
- Consider: How would you enable a public mints accepting FungibleTokens?
- Can you think of any other use cases for attachments?
- Think about this - we just unlocked the ability for an NFT to own other NFTs
- Now extend it - why just own one NFT? What about a whole Collection? What else could NFTs own?
- Take a look at Flowverse projects and start building!
- Think about this - we just unlocked the ability for an NFT to own other NFTs