• Stars
    star
    459
  • Rank 95,377 (Top 2 %)
  • Language Cadence
  • License
    The Unlicense
  • Created over 4 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

The non-fungible token standard on the Flow blockchain

Flow Non-Fungible Token Standard

This standard defines the minimum functionality required to implement a safe, secure, and easy-to-use non-fungible token contract on the Flow blockchain.

What is Cadence?

Cadence is the resource-oriented programming language for developing smart contracts on Flow.

Before reading this standard, we recommend completing the Cadence tutorials to build a basic understanding of the programming language.

Resource-oriented programming, and by extension Cadence, provides an ideal programming model for non-fungible tokens (NFTs). Users are able to store their NFT objects directly in their accounts and transact peer-to-peer. Learn more in this blog post about resources.

Import Addresses

The NonFungibleToken and MetadataViews contracts are already deployed on various networks. You can import them in your contracts from these addresses. There is no need to deploy them yourself.

Note: With the emulator, you must use the -contracts flag to deploy these contracts.

Network Contract Address
Emulator/Canary 0xf8d6e0586b0a20c7
Testnet 0x631e88ae7f1d7c20
Mainnet 0x1d7e57aa55817448

Core features

The NonFungibleToken contract defines the following set of functionality that must be included in each implementation.

Contracts that implement the NonFungibleToken interface are required to implement two resource interfaces:

  • NFT - A resource that describes the structure of a single NFT.

  • Collection - A resource that can hold multiple NFTs of the same type.

    Users typically store one collection per NFT type, saved at a well-known location in their account storage.

    For example, all NBA Top Shot Moments owned by a single user are held in a TopShot.Collection stored in their account at the path /storage/MomentCollection.

Create a new NFT collection

Create a new collection using the createEmptyCollection function.

This function MUST return an empty collection that contains no NFTs.

Users typically save new collections to a well-known location in their account and link the NonFungibleToken.CollectionPublic interface as a public capability.

let collection <- ExampleNFT.createEmptyCollection()

account.save(<-collection, to: /storage/ExampleNFTCollection)

// create a public capability for the collection
account.link<&{NonFungibleToken.CollectionPublic}>(
    /public/ExampleNFTCollection,
    target: /storage/ExampleNFTCollection
)

Withdraw an NFT

Withdraw an NFT from a Collection using the withdraw function. This function emits the Withdraw event.

let collectionRef = account.borrow<&ExampleNFT.Collection>(from: /storage/ExampleNFTCollection)
    ?? panic("Could not borrow a reference to the owner's collection")

// withdraw the NFT from the owner's collection
let nft <- collectionRef.withdraw(withdrawID: 42)

Deposit an NFT

Deposit an NFT into a Collection using the deposit function. This function emits the Deposit event.

This function is available on the NonFungibleToken.CollectionPublic interface, which accounts publish as public capability. This capability allows anybody to deposit an NFT into a collection without accessing the entire collection.

let nft: ExampleNFT.NFT

// ...

let collection = account.getCapability(/public/ExampleNFTCollection)
    .borrow<&{NonFungibleToken.CollectionPublic}>()
    ?? panic("Could not borrow a reference to the receiver's collection")

collection.deposit(token: <-nft)

⚠️ Important

In order to comply with the deposit function in the interface, an implementation MUST take a @NonFungibleToken.NFT resource as an argument. This means that anyone can send a resource object that conforms to @NonFungibleToken.NFT to a deposit function. In an implementation, you MUST cast the token as your specific token type before depositing it or you will deposit another token type into your collection. For example:

let token <- token as! @ExampleNFT.NFT

List NFTs in an account

Return a list of NFTs in a Collection using the getIDs function.

This function is available on the NonFungibleToken.CollectionPublic interface, which accounts publish as public capability.

let collection = account.getCapability(/public/ExampleNFTCollection)
    .borrow<&{NonFungibleToken.CollectionPublic}>()
    ?? panic("Could not borrow a reference to the receiver's collection")

let ids = collection.getIDs()

NFT Metadata

NFT metadata is represented in a flexible and modular way using the standard proposed in FLIP-0636.

When writing an NFT contract, you should implement the MetadataViews.Resolver interface, which allows your NFT to implement one or more metadata types called views.

Each view represents a different type of metadata, such as an on-chain creator biography or an off-chain video clip. Views do not specify or require how to store your metadata, they only specify the format to query and return them, so projects can still be flexible with how they store their data.

How to read metadata

This example shows how to read basic information about an NFT including the name, description, image and owner.

Source: get_nft_metadata.cdc

import ExampleNFT from "..."
import MetadataViews from "..."

// ...

// Get the regular public capability
let collection = account.getCapability(ExampleNFT.CollectionPublicPath)
    .borrow<&{ExampleNFT.ExampleNFTCollectionPublic}>()
    ?? panic("Could not borrow a reference to the collection")

// Borrow a reference to the NFT as usual
let nft = collection.borrowExampleNFT(id: 42)
    ?? panic("Could not borrow a reference to the NFT")

// Call the resolveView method
// Provide the type of the view that you want to resolve
// View types are defined in the MetadataViews contract
// You can see if an NFT supports a specific view type by using the `getViews()` method
if let view = nft.resolveView(Type<MetadataViews.Display>()) {
    let display = view as! MetadataViews.Display

    log(display.name)
    log(display.description)
    log(display.thumbnail)
}

// The owner is stored directly on the NFT object
let owner: Address = nft.owner!.address!

// Inspect the type of this NFT to verify its origin
let nftType = nft.getType()

// `nftType.identifier` is `A.e03daebed8ca0615.ExampleNFT.NFT`

How to implement metadata

The example NFT contract shows how to implement metadata views.

List of views

Name Purpose Status Source Core view
NFTView Basic view that includes the name, description and thumbnail. Implemented MetadataViews.cdc
Display Return the basic representation of an NFT. Implemented MetadataViews.cdc
HTTPFile A file available at an HTTP(S) URL. Implemented MetadataViews.cdc
IPFSFile A file stored in IPFS. Implemented MetadataViews.cdc
Edition Return information about one or more editions for an NFT. Implemented MetadataViews.cdc
Editions Wrapper for multiple edition views. Implemented MetadataViews.cdc
Serial Serial number for an NFT. Implemented MetadataViews.cdc
Royalty A Royalty Cut for a given NFT. Implemented MetadataViews.cdc
Royalties Wrapper for multiple Royalty views. Implemented MetadataViews.cdc
Media Represents a file with a corresponding mediaType Implemented MetadataViews.cdc
Medias Wrapper for multiple Media views. Implemented MetadataViews.cdc
License Represents a license according to https://spdx.org/licenses/ Implemented MetadataViews.cdc
ExternalURL Exposes a URL to an NFT on an external site. Implemented MetadataViews.cdc
NFTCollectionData Provides storage and retrieval information of an NFT Implemented MetadataViews.cdc
NFTCollectionDisplay Returns the basic representation of an NFT's Collection. Implemented MetadataViews.cdc
Rarity Expose rarity information for an NFT Implemented MetadataViews.cdc
Trait Represents a single field of metadata on an NFT. Implemented MetadataViews.cdc
Traits Wrapper for multiple Trait views Implemented MetadataViews.cdc

Core views

The views marked as Core views are considered the minimum required views to provide a full picture of any NFT. If you want your NFT to be featured on the Flow NFT Catalog it should implement all of them as a pre-requisite.

Always prefer wrappers over single views

When exposing a view that could have multiple occurrences on a single NFT, such as Edition, Royalty, Media or Trait the wrapper view should always be used, even if there is only a single occurrence. The wrapper view is always the plural version of the single view name and can be found below the main view definition in the MetadataViews contract.

When resolving the view, the wrapper view should be the returned value, instead of returning the single view or just an array of several occurrences of the view.

Example

Preferred

pub fun resolveView(_ view: Type): AnyStruct? {
    switch view {
        case Type<MetadataViews.Editions>():
            let editionInfo = MetadataViews.Edition(name: "Example NFT Edition", number: self.id, max: nil)
            let editionList: [MetadataViews.Edition] = [editionInfo]
            return MetadataViews.Editions(
                editionList
            )
    }
}

To be avoided

// `resolveView` should always return the same type that was passed to it as an argument, so this is improper usage because it returns `Edition` instead of `Editions`.
pub fun resolveView(_ view: Type): AnyStruct? {
    switch view {
        case Type<MetadataViews.Editions>():
            let editionInfo = MetadataViews.Edition(name: "Example NFT Edition", number: self.id, max: nil)
            return editionInfo
    }
}
// This is also improper usage because it returns `[Edition]` instead of `Editions`
pub fun resolveView(_ view: Type): AnyStruct? {
    switch view {
        case Type<MetadataViews.Editions>():
            let editionInfo = MetadataViews.Edition(name: "Example NFT Edition", number: self.id, max: nil)
            let editionList: [MetadataViews.Edition] = [editionInfo]
            return editionList
    }
}

Royalty View

The MetadataViews contract also includes a standard view for Royalties.

This view is meant to be used by 3rd party marketplaces to take a cut of the proceeds of an NFT sale and send it to the author of a certain NFT. Each NFT can have its own royalty view:

pub struct Royalties {

    /// Array that tracks the individual royalties
    access(self) let cutInfos: [Royalty]
}

and the royalty can indicate whatever fungible token it wants to accept via the type of the generic {FungibleToken.Receiver} capability that it specifies:

pub struct Royalty {
    /// Generic FungibleToken Receiver for the beneficiary of the royalty
    /// Can get the concrete type of the receiver with receiver.getType()
    /// Recommendation - Users should create a new link for a FlowToken receiver for this using `getRoyaltyReceiverPublicPath()`,
    /// and not use the default FlowToken receiver.
    /// This will allow users to update the capability in the future to use a more generic capability
    pub let receiver: Capability<&AnyResource{FungibleToken.Receiver}>

    /// Multiplier used to calculate the amount of sale value transferred to royalty receiver.
    /// Note - It should be between 0.0 and 1.0
    /// Ex - If the sale value is x and multiplier is 0.56 then the royalty value would be 0.56 * x.
    ///
    /// Generally percentage get represented in terms of basis points
    /// in solidity based smart contracts while cadence offers `UFix64` that already supports
    /// the basis points use case because its operations
    /// are entirely deterministic integer operations and support up to 8 points of precision.
    pub let cut: UFix64
}

If someone wants to make a listing for their NFT on a marketplace, the marketplace can check to see if the royalty receiver accepts the seller's desired fungible token by checking the concrete type of the reference. If the concrete type is not the same as the type of token the seller wants to accept, the marketplace has a few options. They could either get the address of the receiver by using the receiver.owner.address field and check to see if the account has a receiver for the desired token, they could perform the sale without a royalty cut, or they could abort the sale since the token type isn't accepted by the royalty beneficiary.

You can see example implementations of royalties in the ExampleNFT contract and the associated transactions and scripts.

Important instructions for royalty receivers

If you plan to set your account as a receiver of royalties, you'll likely want to be able to accept as many token types as possible. This won't be immediately possible at first, but eventually, we will also design a contract that can act as a sort of switchboard for fungible tokens. It will accept any generic fungible token and route it to the correct vault in your account. This hasn't been built yet, but you can still set up your account to be ready for it in the future. Therefore, if you want to receive royalties, you should set up your account with the setup_account_to_receive_royalty.cdc transaction.

This will link generic public path from MetadataViews.getRoyaltyReceiverPublicPath() to your chosen fungible token for now. Then, use that public path for your royalty receiver and in the future, you will be able to easily update the link at that path to use the fungible token switchboard instead.

Contract metadata

Now that contract borrowing is released, you can also implement the Resolver interface on your contract and resolve views from there. As an example, you might want to allow your contract to resolve NFTCollectionData and NFTCollectionDisplay so that platforms do not need to find an NFT that belongs to your contract to get information about how to set up or show your collection.

import ViewResolver from 0xf8d6e0586b0a20c7
import MetadataViews from 0xf8d6e0586b0a20c7

pub fun main(addr: Address, name: String): StoragePath? {
  let t = Type<MetadataViews.NFTCollectionData>()
  let borrowedContract = getAccount(addr).contracts.borrow<&ViewResolver>(name: name) ?? panic("contract could not be borrowed")

  let view = borrowedContract.resolveView(t)
  if view == nil {
    return nil
  }

  let cd = view! as! MetadataViews.NFTCollectionData
  return cd.storagePath
}

Will Return

{"domain":"storage","identifier":"exampleNFTCollection"}

How to propose a new view

Please open a pull request to propose a new metadata view or changes to an existing view.

Feedback

As Flow and Cadence are still new, we expect this standard to evolve based on feedback from both developers and users.

We'd love to hear from anyone who has feedback. For example:

  • Are there any features that are missing from the standard?
  • Are the current features defined in the best way possible?
  • Are there any pre and post conditions that are missing?
  • Are the pre and post conditions defined well enough? Error messages?
  • Are there any other actions that need an event defined for them?
  • Are the current event definitions clear enough and do they provide enough information?
  • Are the variable, function, and parameter names descriptive enough?
  • Are there any openings for bugs or vulnerabilities that we are not noticing?

Please create an issue in this repository if there is a feature that you believe needs discussing or changing.

Comparison to other standards on Ethereum

This standard covers much of the same ground as ERC-721 and ERC-1155, but without most of the downsides.

  • Tokens cannot be sent to contracts that don't understand how to use them, because an account needs to have a Receiver or Collection in its storage to receive tokens.
  • If the recipient is a contract that has a stored Collection, the tokens can just be deposited to that Collection without having to do a clunky approve, transferFrom.
  • Events are defined in the contract for withdrawing and depositing, so a recipient will always be notified that someone has sent them tokens with their own deposit event.
  • This version can support batch transfers of NFTs. Even though it isn't explicitly defined in the contract, a batch transfer can be done within a transaction by just withdrawing all the tokens to transfer, then depositing them wherever they need to be, all atomically.
  • Transfers can trigger actions because users can define custom Receivers to execute certain code when a token is sent.
  • Easy ownership indexing: rather than iterating through all tokens to find which ones you own, you have them all stored in your account's collection and can get the list of the ones you own instantly.

How to test the standard

If you want to test out these contracts, we recommend either testing them with the Flow Playground or with the Visual Studio Code Extension.

The steps to follow are:

  1. Deploy NonFungibleToken.cdc
  2. Deploy ExampleNFT.cdc, importing NonFungibleToken from the address you deployed it to.

Then you can experiment with some of the other transactions and scripts in transactions/ or even write your own. You'll need to replace some of the import address placeholders with addresses that you deploy to, as well as some of the transaction arguments.

Running automated tests

You can find automated tests in the lib/go/test/nft_test.go file. It uses the transaction templates that are contained in the lib/go/templates/templates.go file. Currently, these rely on a dependency from a private dapper labs repository to run, so external users will not be able to run them. We are working on making all of this public so anyone can run tests, but haven't completed this work yet.

Bonus features

(These could each be defined as a separate interface and standard and are probably not part of the main standard) They are not implemented in this repository yet

10- Withdrawing tokens from someone else's Collection by using their Provider reference.

  • approved withdraw event
  • Providing a resource that only approves an account to withdraw a specific amount per transaction or per day/month/etc.
  • Returning the list of tokens that an account can withdraw for another account.
  • Reading the balance of the account that you have permission to send tokens for
  • Owner is able to increase and decrease the approval at will, or revoke it completely
    • This is much harder than anticipated

11 - Standard for Composability/Extensibility

12 - Minting a specific amount of tokens using a specific minter resource that an owner can control

  • tokens minted event
  • Setting a cap on the total number of tokens that can be minted at a time or overall
  • Setting a time frame where this is allowed

13 - Burning a specific amount of tokens using a specific burner resource that an owner controls

  • tokens burnt event
  • Setting a cap on the number of tokens that can be burned at a time or overall
  • Setting a time frame where this is allowed

14 - Pausing Token transfers (maybe a way to prevent the contract from being imported? probably not a good idea)

15 - Cloning the token to create a new token with the same distribution

License

The works in these files:

are under the Unlicense.

Deploying updates

Testnet

TESTNET_PRIVATE_KEY=xxxx flow project deploy --update --network testnet

More Repositories

1

flow-go

A fast, secure, and developer-friendly blockchain built to support the next generation of games, apps, and the digital assets that power them.
Go
532
star
2

cadence

Cadence, the resource-oriented smart contract programming language 🏃‍♂️
Go
505
star
3

kitty-items

Kitty Items: CryptoKitties Sample App
JavaScript
408
star
4

flow

Flow is a fast, secure, and developer-friendly blockchain built to support the next generation of games, apps, and the digital assets that power them 🌊
Go
357
star
5

fcl-js

FCL (Flow Client Library) - The best tool for building JavaScript (browser & NodeJS) applications on Flow 🌊
JavaScript
318
star
6

flow-go-sdk

Tools for building Go applications on Flow 🌊
Go
211
star
7

flow-cli

The Flow CLI is a command-line interface that provides useful utilities for building Flow applications
Go
207
star
8

flow-ft

The Fungible Token standard on the Flow Blockchain
Cadence
136
star
9

flow-playground

Flow Playground front-end app 🤹‍♂️
TypeScript
112
star
10

nft-storefront

A general-purpose Cadence contract for trading NFTs on Flow
Cadence
102
star
11

flow-core-contracts

Cadence smart contracts that define core functionality of the Flow protocol
Go
86
star
12

flow-emulator

The Flow Emulator is a lightweight tool that emulates the behaviour of the real Flow network
Go
84
star
13

freshmint

TypeScript
68
star
14

vscode-cadence

The Visual Studio Code extension for Cadence
TypeScript
52
star
15

fcl-dev-wallet

A Flow wallet for development purposes. To be used with the Flow Emulator.
Go
50
star
16

developer-grants

Grants for developers that contribute to the broader developer ecosystem
50
star
17

flip-fest

A backlog of all the available tasks to complete for Flow's FLIP Fest.
50
star
18

flow-js-testing

Testing framework to enable Cadence testing via a set of JavaScript methods and tools
JavaScript
46
star
19

atree

Atree provides scalable arrays and scalable ordered maps.
Go
39
star
20

nft-catalog

Cadence
37
star
21

flow-101-quest

Cadence
30
star
22

flips

Flow Improvement Proposals
25
star
23

cadence-tools

Developer tools for Cadence
Go
24
star
24

flow-cadut

Node based template generator to simplify interaction with Cadence files.
JavaScript
22
star
25

wallet-api

A REST API to create and manage Flow accounts
HTML
22
star
26

walletless-arcade-example

An example Flow App to demonstrate walletless onboarding and promote composable, secure, and smooth UX for on-chain games without the need for a traditional backend.
Cadence
21
star
27

monster-maker

Create a monster on Flow
TypeScript
18
star
28

hybrid-custody

Cadence suite enabling Hybrid Custody on Flow
Cadence
15
star
29

rosetta

Rosetta implementation for the Flow blockchain
Go
15
star
30

UnityFlowSDK

Flow SDK for Unity - build Unity games on the Flow blockchain
C#
13
star
31

flow-java-client-example

Java
12
star
32

wallet-extension-example

An example and guide showing how to build an FCL-compatible wallet extension on Flow.
JavaScript
12
star
33

fcl-discovery

JavaScript
11
star
34

flow-interaction-template-service

TypeScript
11
star
35

flow-playground-api

Flow Playground back-end app 🤹‍♂️
Go
11
star
36

linked-accounts

Cadence
11
star
37

ledger-app-flow

JavaScript
10
star
38

sc-eng-gaming

Showcasing how a Rock Paper Scissors game could be made #onFlow
Cadence
10
star
39

flow-batch-scan

Library for running batch scans of the flow network
Go
10
star
40

flow-evm-gateway

FlowEVM Gateway implements an Ethereum-equivalent JSON-RPC API for EVM clients to use
Go
9
star
41

cadence-libraries

Libraries for common programming utilities in the Cadence smart contract programming language
Go
9
star
42

flow-evm-bridge

Repository for contracts supporting bridge between Flow <> EVM
Cadence
9
star
43

sdks

Tips and resources for building Flow SDKs and client libraries 🔧
Cadence
9
star
44

developer-portal

Source code for developers.flow.com
TypeScript
8
star
45

docs

Flow Developer Portal. Discover the developer ecosystem and master the Flow blockchain
TypeScript
8
star
46

cadence-lang.org

The home of the Cadence website
MDX
8
star
47

cadence-cookbook

JavaScript
8
star
48

flow-multisig

JavaScript
8
star
49

OWBSummer2020Project

OWB (Open World Builders) bootcamp summer 2020 cohort capstone projects submission
CSS
8
star
50

Flow-Working-Groups

8
star
51

casestudies

Learn more about building on Flow from teams that have already launched successful projects
7
star
52

BFTune

Byzantine Fault Tolerance (BFT) Testing Framework for Flow
7
star
53

cadence-style-guide

The Flow Team guide on how to write better Cadence code
7
star
54

twg

Tokenomics Working Group
6
star
55

flowkit

Go
6
star
56

flow-interaction-template-tools

TypeScript
6
star
57

flow-kmm

Kotlin Multiplatform Mobile (KMM) library for building Android and iOS applications on the Flow blockchain
Objective-C
5
star
58

scaffold-flow

🌊 forkable Flow dev stack focused on fast product iterations
JavaScript
5
star
59

flow-issue-box

A public repository dedicated to collecting issues and feedback from external contributors. The Issue Box 🗳️ is similar to a backlog, where the issue is later triaged and assigned to the proper team.
5
star
60

ccf

Cadence Compact Format (CCF) is a binary data format and alternative to JSON-CDC
4
star
61

token-cold-storage

JavaScript
4
star
62

cddl

A CDDL implementation in Go
Go
4
star
63

Offers

Cadence
4
star
64

full-observer-node-example

An example demonstrating the use of the Flow unstaked consensus follower
Go
4
star
65

flow-account-api

Go
4
star
66

faucet

Cadence
4
star
67

contract-updater

Enabling delayed contract updates to a wrapped account at or beyond a specified block height
Cadence
4
star
68

flow-validator

Run a Flow node and help secure the network
4
star
69

flow-playground-tutorials

Repository to contain and test tutorials, which are deployed to Playground and used in docs
Cadence
3
star
70

flow-public-key-indexer

A observer service for indexing flow Accounts and by their associated Keys.
Go
3
star
71

flow-cadence-eth-utils

Cadence
3
star
72

flow-cadence-hrt

Write your Cadence tests with human readable TOML
JavaScript
3
star
73

random-coin-toss

An example repo demonstrating safe use of onchain randomness
Cadence
3
star
74

blindninja

Blind Ninja is a composability-first fully onchain game on Flow
JavaScript
3
star
75

waterhose

Waterhose listens to Flow events and publishes them to Kafka
2
star
76

fcl-auth-android

A Kotlin library for the Flow Client Library (FCL) that enables Flow wallet authentication on Android devices
Kotlin
2
star
77

service-account

Cadence
2
star
78

fcl-six

Stored Interactions (SIX) for FCL
JavaScript
2
star
79

flow-scaffold-list

Scaffolds for starting Flow apps
2
star
80

hybrid-custody-scaffold

Starter template for working with HybridCustody contract suite
Cadence
2
star
81

fcl-contracts

Cadence contracts used by the Flow Client Library (FCL)
Cadence
2
star
82

crypto

Assembly
2
star
83

dex

2
star
84

gwg

The Flow Governance Working Group (GWG) is an open-for-all forum that plays a pivotal role in shaping the Flow ecosystem with a focus on discussing and resolving governance issues.
2
star
85

next-docs-v0

next-docs
JavaScript
1
star
86

storage-fees-scripts

1
star
87

unity-flow-sdk-api-docs

HTML
1
star
88

fusd

Cadence source code for the FUSD stablecoin on Flow
Cadence
1
star
89

band-oracle-contracts

Contracts and related for integration with Band protocol Oracle Network
Cadence
1
star
90

cadence-1-migrator

Cadence
1
star
91

fcl-gcp-kms-web

JavaScript
1
star
92

fcl-next-scaffold

JavaScript
1
star
93

wallet-wg

Flow Wallet Working Group
1
star
94

minting-nfts-in-a-set

Cadence Cookbook Recipe: Minting NFTs in a Set
Cadence
1
star
95

create-a-marketplace

Cadence Cookbook Recipe: Create a Marketplace
Cadence
1
star
96

metadata-views

Cadence Cookbook Recipe: Metadata Views
Cadence
1
star
97

create-a-topshot-play

Cadence Cookbook Recipe: Create a TopShot Play
Cadence
1
star
98

collection-for-holding-nfts

Cadence Cookbook Recipe: Collection for Holding NFTs
Cadence
1
star
99

add-a-play-to-topshot-set

Cadence Cookbook Recipe: Add a Play to TopShot Set
Cadence
1
star
100

minting-a-moment-in-topshot-set

Cadence Cookbook Recipe: Minting a Moment in TopShot Set
Cadence
1
star