如何使用Cadence在Flow上创建NFT集合dApp

  • QuickNode
  • 发布于 2024-10-17 17:21
  • 阅读 32

本文是一篇关于如何在Flow区块链上创建NFT集合dApp的教程。它介绍了Flow区块链的关键特性,如多角色架构和资源导向编程,以及Cadence编程语言。该教程指导读者使用Cadence、Flow CLI和NextJS创建一个NFT集合智能合约,并构建一个用于铸造NFT的前端界面。

概述

Flow 是一个快速、去中心化且对开发者友好的 L1 区块链,专为下一代游戏、应用程序和数字资产而设计。Flow 的多角色架构专为互联网规模的性能而构建,具有低费用、可升级的智能合约以及用户友好的工具,如 Flow Emulator 和 flow-cli。Cadence 是 Flow 的下一代智能合约语言,它将数字资产直接存储在帐户中,从而保证了对所持有资产的直接所有权。Cadence 结合了安全性和其他语言原生概念来处理链上数字资产,使开发者能够专注于构建产品。合约可以调用其他合约,即使是动态调用也可以,交易是 ACID 的,脚本简化了对链上数据的读取访问。创建复杂、去中心化的应用程序,将数据和逻辑一起存储在链上从未如此简单。Flow 前所未有地释放了 Web3 的生产力提升和真正的可组合性。

在本指南中,你将了解 Flow。在这里,你将学习如何创建一个 NFT 集合并使用 Cadence、Flow CLI 和 NextJS 构建一个前端。

虽然我们的探索之旅发生在 Flow Testnet 的沙盒中,但请放心,你将获得的技能可以完全转移到 Flow Mainnet。让我们扭转局面,一起将复杂变为简单!

你需要的准备

你可以通过点击上面的链接并按照说明下载和安装每个依赖项。

本指南中使用的依赖项

依赖项 版本
node.js 18.16.0
flow-cli 1.4.2

你将要做什么

  • 了解 Flow 区块链
  • 了解 Cadence 编程语言
  • 使用 Flow CLI 创建 Flow 钱包
  • 使用 Cadence 创建 NFT 集合智能合约、交易和脚本文件
  • 将智能合约部署到 Flow 区块链上
  • 构建一个用于铸造 NFT 的前端

Flow 区块链概述

Flow 的主要特性

多角色架构

Flow 通过将验证器节点的工作分成四个不同的角色:收集、共识、执行和验证,从而将流水线应用于交易处理。这种独特的多节点架构允许 Flow 在不影响网络安全或长期去中心化的情况下显著扩展。如果需要,可以在此处找到更多详细信息。

面向资源的编程

Flow 采用面向资源的编程是智能合约领域的一个游戏规则改变者。Cadence 允许开发者定义自己的 Resource 类型,并强制执行其使用的自定义规则。Cadence 的设计降低了其他智能合约语言中编程错误和漏洞的风险,因为资源无法被复制或销毁,只能存储在帐户中。在代码执行期间,如果 Resource 在完成后未存储在帐户中,函数将中止。

对于熟悉 Solidity 的开发者来说,Cadence 引入了许多新的概念和模式,这些概念和模式可能与你习惯的不同。我们建议查看面向 Solidity 开发者指南 (Guide for Solidity developers),以更详细地了解这些差异。

帐户概念

Flow 的帐户模型将每个帐户定义为与地址相关联,并持有一个或多个公钥、合约和其他帐户存储。更多详细信息可以在 Flow 的开发者文档中的 帐户 中找到。

  • 地址:帐户的唯一标识。
  • 公钥:已在帐户上批准的公钥。
  • 合约:已部署到帐户的 Cadence 合约。
  • 存储:帐户中保存资源资产的区域。

Flow 帐户支持多个公钥,从而在访问控制方面提供了灵活性,并且相应私钥的所有者可以签署交易以修改帐户的状态,从而提供了与以太坊的单密钥对帐户模型不同的方式。

可升级性

基于 EVM 的链始终强制执行合约的不可变性,这促使开发者使用代理模式来解决该限制。相比之下,Flow 允许开发者在有限程度上升级智能合约。

为什么要在 Flow 上创建 NFT 集合?

不可替代代币 (NFT) 席卷了数字世界,彻底改变了数字领域的所有权和创造力。虽然 NFT 通常与以太坊的 ERC-721 和 ERC-1155 标准相关联,但 Flow 以其独特的优势提供了一个引人注目的替代方案。本节探讨了创作者和收藏家应该考虑在 Flow 上创建 NFT 集合的原因。

开发者工具

Flow 通过易于使用的开发者和测试工具、SDK、钱包、Flow Playground、内置于 VS Code 和 GoLand 中的 Cadence 支持以及更多其他来提高开发者生产力。再加上 Flow 广泛的开发者文档,在 Flow 上构建的体验既高效又愉快!

面向资源的编程和 Cadence

资源在 Cadence 中专门用于表示供应有限的资产或代币,使其成为区块链的理想选择。线性类型,正如这种类型概念所知,是指编程语言强制执行每个变量只能存在一次并且是唯一的约束。NFT 本身就是一种资源类型 - 使它们成为 NFT 的原因是它们遵守以下详述的标准。但是,资源具有无限范围的可能代币化用例,这些用例可能对身份、权利分配或其他用途有用。涉及资产交换的交易,例如出售 NFT 以换取可替代代币资源,以点对点方式处理 - 发送者资产直接存入接收者,反之亦然,而不是通过中央合约进行中介。

去中心化所有权: Flow 上的所有存储都完全在帐户内部处理。与基于账本的区块链不同,拥有的资源不是资产 ID 到所有者 ID 的映射。当资产位于给定的帐户中时,只有该帐户持有人才能采取行动将该资源移动到其他地方。

基于能力的访问控制: Cadence 使用 Capabilities 来限制对受保护函数或对象的访问。Capability 是一种类似于密钥的实体,提前提供给持有帐户,仅允许他们启动特定操作。Capability 可以直接提供给特定帐户,或者在需要时撤销。

不可替代代币标准

Flow 的 NFT 生态系统的核心是 NonFungibleToken 合约,该合约定义了一组 NFT 的基本功能。Flow 上的任何 NonFungibleToken 接口的实现都需要实现两个资源接口:

1. NFT - 描述单个 NFT 的资源: 此资源概述了单个 NFT 的结构,包括其独特的特征和属性。

2. Collection - 用于保存相同类型的多个 NFT 的资源: 集合是可以存储相同类型的多个 NFT 的存储库。用户通常为每种 NFT 类型维护一个集合,该集合存储在其帐户存储中的预定义位置。

例如,想象一下用户拥有 NBA Top Shot Moments NFT。这些 NFT 将存储在用户帐户中路径 /storage/MomentCollection 的 TopShot.Collection 中。

强大的 NFT 生态系统

Flow 已确立了自己作为主流就绪的区块链的地位,并以体育 NFT 项目而闻名,例如 NBA TopShotNFL AllDayUFC Strike,这些项目允许用户拥有和交易来自他们最喜欢的运动的不同稀有度的数字时刻。成百上千的其他 NFT 项目也在 Flow 上启动,包括 DoodlesFlovatar许多其他

开发者设置

使用 QuickNode 访问 Flow

要在 Flow 上构建,你需要一个端点来连接 Flow 网络。欢迎你使用由 QuickNode 提供支持的 Flow 公共端点或部署和管理你自己的基础设施;但是,如果你想要更快的响应时间,你可以将繁重的工作交给我们。了解为什么 Flow 选择 QuickNode 作为其公共节点的提供商并在此处注册一个帐户 here

在本指南中,我们将使用由 QuickNode 提供支持的公共端点。

设置你的开发环境

你需要一个终端模拟器(即,终端、Windows PowerShell)和一个代码编辑器(即,Visual Studio Code)来设置项目。

我们假设你已经安装了 Flow-CLI,因为它在你需要什么准备中已被提及,但如果没有,请检查相关部分以安装必要的程序和软件包。

创建一个 Flow 项目

在你的终端中运行以下代码来设置项目。我们的项目文件夹的名称将是 nft-collection-qn,但你可以根据需要修改名称。--scaffold 标志用于使用提供的 scaffolds,这些是你可用于引导你的开发的工程模版。

flow setup nft-collection-qn --scaffold

如果终端需要你输入 scaffold number,你可以选择第五个选项 [5] FCL Web Dapp。它会创建所有必要的文件和文件夹,以使用 next.js、FCL 和 Cadence 构建一个简单的 TypeScript Web 应用程序。控制台输出应如下所示。

Enter the scaffold number: 5

🎉 Congrats! your project was created.

Start development by following these steps:
1. 'cd nft-collection-qn' to change to your new project,
2. 'flow emulator' or run Flowser to start the emulator,
3. 'flow dev' to start developing.

You should also read README.md to learn more about the development process!

运行命令来更改你的目录。

cd nft-collection-qn

创建了项目文件夹后,你现在可以继续创建 Flow 钱包。

配置你的 Flow 帐户

你必须使用 Flow CLI 在 Flow 区块链上创建一个帐户才能部署智能合约。幸运的是,Flow CLI 有一个有用的命令来自动创建钱包并为其提供资金。

要设置钱包,请在你的终端中运行以下代码。

flow accounts create

然后,输入一个帐户名并选择网络。我们输入 testnet-account 作为帐户名,并选择 "Testnet" 作为网络。

控制台输出应如下所示。

🎉 New account created with address 0xbcc2fbf2808c44b6 and name testnet-account on Testnet network.

Here’s a summary of all the actions that were taken:
 - Added the new account to flow.json.
 - Saved the private key to testnet-account.pkey.
 - Added testnet-account.pkey to .gitignore.

保存帐户地址,因为它对于以下部分是必需的。

正如输出中所述,此命令执行以下操作:

  • 将新帐户添加到 flow.json 文件
  • 将私钥保存到 pkey 文件
  • pkey 文件添加到 .gitignore 文件

完成此步骤后,你的帐户已创建并已提供资金。

检查配置文件

Flow 配置文件 (flow.json) 用于定义网络 (即主网、测试网)、帐户、部署目标以及将要部署的合约。因此,以下属性应包含在配置文件中:

  • networks 预定义 Flow 模拟器、测试网和主网连接配置
  • accounts 预定义 Flow 模拟器帐户和你新创建的帐户
  • deployments 可以定义所有部署目标
  • contracts 可以定义项目中将使用的所有合约

文件的默认状态可能没有所有提到的属性。这不是问题;当我们准备好部署时,我们将逐步更新文件。

当我们之前使用 flow accounts create 命令创建一个钱包时,我们的帐户会自动添加到此配置文件中。但是,如果你有想要导入的钱包凭据,你可以在此处添加它们。(详情请参阅 Flow CLI 配置 文档。)

现在无需更新此文件中的任何内容,但我们强烈建议你查看该文件以更好地理解配置文件。

到目前为止,文件夹结构应如下所示。

├── README.md              # Documentation for your app.
├── cadence                # Cadence language files (smart contracts, transactions, and scripts).
├── components             # React components used in your app.
├── config                 # Configuration files for your app.
├── constants              # Constants used in your app.
├── emulator.key           # Emulator key file (for local development).
├── flow.json              # Flow blockchain configuration or settings.
├── helpers                # Helper functions or utilities for your app.
├── hooks                  # Custom React hooks used in your app.
├── jest.config.js         # Configuration file for Jest testing framework.
├── layouts                # Layout components for your app.
├── next.config.js         # Configuration file for Next.js.
├── package-lock.json      # Automatically generated file to lock dependency versions.
├── package.json           # Package configuration file for Node.js.
├── pages                  # Next.js pages for routing.
├── public                 # Static files served by Next.js.
├── styles                 # Stylesheets and CSS for your app.
├── testnet-account.pkey   # Testnet account private key (for testing purposes).
├── tsconfig.json          # TypeScript configuration for your app.
└── types                  # TypeScript type declarations for your app.

在 Flow 上创建一个 NFT 集合

对于 NFT 集合 dApp,我们需要以下文件:

  • 智能合约 (QuickNFT.cdc)
  • 脚本文件 (GetIDsQuickNFT.cdcTotalSupplyQuickNFT.cdcGetMetadataQuickNFT.cdc)
  • 交易文件 (MintNFT.cdcSetUpAccount.cdc)

info

脚本:脚本是可执行的 Cadence 代码,用于查询 Flow 网络但不修改它。与 Flow 交易不同,它们不需要签名,并且可以返回值。你可以将执行脚本视为只读操作。

交易:交易是经过密码学签名的数据消息,其中包含一组更新 Flow 状态的指令。它们是由执行节点执行的基本计算单元。为了使交易包含在 Flow 区块链中,需要从付款人处收取费用。

但是,在跳转到编码之前,让我们了解更多关于 Flow 的原生 NFT 标准。

Flow 的原生 NFT 标准

Flow 的原生 NFT 标准,通常被称为 NonFungibleToken 合约,定义了一组基本功能,以确保 NFT 的安全高效管理。该标准提供了一种简化的方法来创建、交易和交互 NFT,突出了 Flow 对简单性和用户友好开发的承诺。

NonFungibleToken 合约的核心功能包括两个基本的资源接口:NFT,它描述了单个 NFT 的结构,以及 Collection,旨在保存相同类型的多个 NFT。用户通常通过在他们的帐户存储中的预定义位置保存每个 NFT 类型的一个集合来组织他们的 NFT。例如,用户可能会将他们所有的 NBA Top Shot Moments 存储在位于 /storage/MomentCollection 的 TopShot.Collection 中。

要创建一个新的 NFT 集合,开发者可以利用 createEmptyCollection 函数,该函数会生成一个没有任何 NFT 的空集合。用户通常将这些新集合保存在他们帐户中可识别的位置,并使用 NonFungibleToken.CollectionPublic 接口来建立一个用于链接的公共 Capability。

当涉及到将 NFT 存入集合时,存款函数就派上用场了。此操作会触发 Deposit 事件,并且可以通过 NonFungibleToken.CollectionPublic 接口访问,允许个人将 NFT 存入集合,而无需访问整个集合。

创建智能合约

转到 ./cadence/contracts 目录。然后,通过运行以下命令在其中创建一个名为 QuickNFT.cdc 的文件。

echo > QuickNFT.cdc

然后,使用你的代码编辑器打开 QuickNFT.cdc 文件。复制下面的代码并将其粘贴到文件中。

此代码可能看起来很复杂,但是我们将在代码片段之后逐步引导你完成它。此外,许多函数是标准的,并在 Flow 的文档 中进行了解释。

import NonFungibleToken from 0x631e88ae7f1d7c20
import ViewResolver from 0x631e88ae7f1d7c20
import MetadataViews from 0x631e88ae7f1d7c20

pub contract QuickNFT: NonFungibleToken, ViewResolver {

    /// Total supply of QuickNFTs in existence
    /// QuickNFT 存在的总供应量
    pub var totalSupply: UInt64

    /// The event that is emitted when the contract is created
    /// 创建合约时发出的事件
    pub event ContractInitialized()

    /// The event that is emitted when an NFT is withdrawn from a Collection
    /// 从集合中提取 NFT 时发出的事件
    pub event Withdraw(id: UInt64, from: Address?)

    /// The event that is emitted when an NFT is deposited to a Collection
    /// 将 NFT 存入集合时发出的事件
    pub event Deposit(id: UInt64, to: Address?)

    /// Storage and Public Paths
    /// 存储和公共路径
    pub let CollectionStoragePath: StoragePath
    pub let CollectionPublicPath: PublicPath

    /// The core resource that represents a Non Fungible Token.
    /// 代表不可替换代币的核心资源。
    /// New instances will be created using the NFTMinter resource
    /// 将使用 NFTMinter 资源创建新实例
    /// and stored in the Collection resource
    /// 并存储在 Collection 资源中
    ///
    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {

        /// The unique ID that each NFT has
        /// 每个 NFT 具有的唯一 ID
        pub let id: UInt64

        /// Metadata fields
        /// 元数据字段
        pub let name: String
        pub let description: String
        pub let thumbnail: String
        access(self) let metadata: {String: AnyStruct}

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

        /// Function that returns all the Metadata Views implemented by a Non Fungible Token
        /// 返回由不可替换代币实现的所有元数据视图的函数
        ///
        /// @return An array of Types defining the implemented views. This value will be used by
        ///         developers to know which parameter to pass to the resolveView() method.
        /// @return 一个定义已实现视图的类型数组。 开发者将使用此值来了解将哪个参数传递给 resolveView() 方法。
        ///
        pub fun getViews(): [Type] {
            return [\
                Type<MetadataViews.Display>(),\
                Type<MetadataViews.Editions>(),\
                Type<MetadataViews.ExternalURL>(),\
                Type<MetadataViews.NFTCollectionData>(),\
                Type<MetadataViews.NFTCollectionDisplay>(),\
                Type<MetadataViews.Serial>(),\
                Type<MetadataViews.Traits>()\
            ]
        }

        /// Function that resolves a metadata view for this token.
        /// 解析此代币的元数据视图的函数。
        ///
        /// @param view: The Type of the desired view.
        /// @param view: 所需视图的类型。
        /// @return A structure representing the requested view.
        /// @return 一个表示所请求视图的结构。
        ///
        pub fun resolveView(_ view: Type): AnyStruct? {
            switch view {
                case Type<MetadataViews.Display>():
                    return MetadataViews.Display(
                        name: self.name,
                        description: self.description,
                        thumbnail: MetadataViews.HTTPFile(
                            url: self.thumbnail
                        )
                    )
                case Type<MetadataViews.Editions>():
                    // There is no max number of NFTs that can be minted from this contract
                    // 因此,可以从此合约中铸造的 NFT 的最大数量未设置
                    // so the max edition field value is set to nil
                    // 因此,最大版本字段值设置为零
                    let editionInfo = MetadataViews.Edition(name: "Example NFT Edition", number: self.id, max: nil)
                    let editionList: [MetadataViews.Edition] = [editionInfo]
                    return MetadataViews.Editions(
                        editionList
                    )
                case Type<MetadataViews.Serial>():
                    return MetadataViews.Serial(
                        self.id
                    )
                case Type<MetadataViews.ExternalURL>():
                    return MetadataViews.ExternalURL("https://example-nft.onflow.org/".concat(self.id.toString()))
                case Type<MetadataViews.NFTCollectionData>():
                    return MetadataViews.NFTCollectionData(
                        storagePath: QuickNFT.CollectionStoragePath,
                        publicPath: QuickNFT.CollectionPublicPath,
                        providerPath: /private/QuickNFTCollection,
                        publicCollection: Type<&QuickNFT.Collection{QuickNFT.QuickNFTCollectionPublic}>(),
                        publicLinkedType: Type<&QuickNFT.Collection{QuickNFT.QuickNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
                        providerLinkedType: Type<&QuickNFT.Collection{QuickNFT.QuickNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
                        createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
                            return <-QuickNFT.createEmptyCollection()
                        })
                    )
                case Type<MetadataViews.NFTCollectionDisplay>():
                    let media = MetadataViews.Media(
                        file: MetadataViews.HTTPFile(
                            url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg"
                        ),
                        mediaType: "image/svg+xml"
                    )
                    return MetadataViews.NFTCollectionDisplay(
                        name: "The Example Collection",
                        description: "This collection is used as an example to help you develop your next Flow NFT.",
                        externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"),
                        squareImage: media,
                        bannerImage: media,
                        socials: {
                            "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain")
                        }
                    )
                case Type<MetadataViews.Traits>():
                    // exclude mintedTime and foo to show other uses of Traits
                    // 排除 mintedTime 和 foo 以显示 Traits 的其他用途
                    let excludedTraits = ["mintedTime", "foo"]
                    let traitsView = MetadataViews.dictToTraits(dict: self.metadata, excludedNames: excludedTraits)

                    // mintedTime is a unix timestamp, we should mark it with a displayType so platforms know how to show it.
                    // mintedTime 是一个 unix 时间戳,我们应该用 displayType 标记它,以便平台知道如何显示它。
                    let mintedTimeTrait = MetadataViews.Trait(name: "mintedTime", value: self.metadata["mintedTime"]!, displayType: "Date", rarity: nil)
                    traitsView.addTrait(mintedTimeTrait)

                    // foo is a trait with its own rarity
                    // foo 是一种具有自身稀有度的性状
                    let fooTraitRarity = MetadataViews.Rarity(score: 10.0, max: 100.0, description: "Common")
                    let fooTrait = MetadataViews.Trait(name: "foo", value: self.metadata["foo"], displayType: nil, rarity: fooTraitRarity)
                    traitsView.addTrait(fooTrait)

                    return traitsView

            }
            return nil
        }
    }

    /// Defines the methods that are particular to this NFT contract collection
    /// 定义此 NFT 签约集合特有的方法
    ///
    pub resource interface QuickNFTCollectionPublic {
        pub fun deposit(token: @NonFungibleToken.NFT)
        pub fun getIDs(): [UInt64]
        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
        pub fun borrowQuickNFT(id: UInt64): &QuickNFT.NFT? {
            post {
                (result == nil) || (result?.id == id):
                    "Cannot borrow QuickNFT reference: the ID of the returned reference is incorrect"
                    "无法借用 QuickNFT 引用: 返回的引用的 ID 不正确"
            }
        }
    }

    /// The resource that will be holding the NFTs inside any account.
    /// 将在任何帐户中持有 NFT 的资源。
    /// In order to be able to manage NFTs any account will need to create
    /// 为了能够管理 NFT,任何帐户都需要创建
    /// an empty collection first
    /// 首先创建一个空集合
    ///
    pub resource Collection: QuickNFTCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
        // dictionary of NFT conforming tokens
        // 符合 NFT 的代币词典
        // NFT is a resource type with an `UInt64` ID field
        // NFT 是一种资源类型,具有一个 `UInt64` ID 字段
        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}

        init () {
            self.ownedNFTs <- {}
        }

        /// Removes an NFT from the collection and moves it to the caller
        /// 从集合中删除 NFT 并将其移动到调用者
        ///
        /// @param withdrawID: The ID of the NFT that wants to be withdrawn
        /// @param withdrawID: 要提取的 NFT 的 ID
        /// @return The NFT resource that has been taken out of the collection
        /// @return 已从集合中取出的 NFT 资源
        ///
        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")

            emit Withdraw(id: token.id, from: self.owner?.address)

            return <-token
        }

        /// Adds an NFT to the collections dictionary and adds the ID to the id array
        /// 将 NFT 添加到集合字典并将 ID 添加到 id 数组
        ///
        /// @param token: The NFT resource to be included in the collection
        /// @param token: 要包含在集合中的 NFT 资源
        ///
        pub fun deposit(token: @NonFungibleToken.NFT) {
            let token <- token as! @QuickNFT.NFT

            let id: UInt64 = token.id

            // add the new token to the dictionary which removes the old one
            // 将新代币添加到字典中,这将删除旧代币
            let oldToken <- self.ownedNFTs[id] <- token

            emit Deposit(id: id, to: self.owner?.address)

            destroy oldToken
        }

        /// Helper method for getting the collection IDs
        /// 用于获取集合 ID 的帮助方法
        ///
        /// @return An array containing the IDs of the NFTs in the collection
        /// @return 一个包含集合中 NFT 的 ID 的数组
        ///
        pub fun getIDs(): [UInt64] {
            return self.ownedNFTs.keys
        }

        /// Gets a reference to an NFT in the collection so that
        /// 获取对集合中 NFT 的引用,以便
        /// the caller can read its metadata and call its methods
        /// 调用者可以读取其元数据并调用其方法
        ///
        /// @param id: The ID of the wanted NFT
        /// @param id: 所需 NFT 的 ID
        /// @return A reference to the wanted NFT resource
        /// @return 对所需 NFT 资源的引用
        ///
        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
        }

        /// Gets a reference to an NFT in the collection so that
        /// 获取对集合中 NFT 的引用,以便
        /// the caller can read its metadata and call its methods
        /// 调用者可以读取其元数据并调用其方法
        ///
        /// @param id: The ID of the wanted NFT
        /// @param id: 所需 NFT 的 ID
        /// @return A reference to the wanted NFT resource
        /// @return 对所需 NFT 资源的引用
        ///
        pub fun borrowQuickNFT(id: UInt64): &QuickNFT.NFT? {
            if self.ownedNFTs[id] != nil {
                // Create an authorized reference to allow downcasting
                // 创建一个授权的引用以允许向下转换
                let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
                return ref as! &QuickNFT.NFT
            }

            return nil
        }

        /// Gets a reference to the NFT only conforming to the `{MetadataViews.Resolver}`
        /// 获取对仅符合 `{MetadataViews.Resolver}` 的 NFT 的引用
        /// interface so that the caller can retrieve the views that the NFT
        /// 接口,以便调用者可以检索 NFT 正在实现的视图并解析它们
        /// is implementing and resolve them
        ///
        /// @param id: The ID of the wanted NFT
        /// @param id: 所需 NFT 的 ID
        /// @return The resource reference conforming to the Resolver interface
        /// @return 符合 Resolver 接口的资源引用
        ///
        pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
            let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
            let QuickNFT = nft as! &QuickNFT.NFT
            return QuickNFT as &AnyResource{MetadataViews.Resolver}
        }

        destroy() {
            destroy self.ownedNFTs
        }
    }

    /// Allows anyone to create a new empty collection
    /// 允许任何人创建一个新的空集合
    ///
    /// @return The new Collection resource
    /// @return 新的 Collection 资源
    ///
    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
        return <- create Collection()
    }

    /// Mints a new NFT with a new ID and deposit it in the
    /// 使用新 ID 铸造一个新的 NFT 并将其存入
    /// recipients collection using their collection reference
    /// 收件人集合,使用他们的集合引用
    ///
    /// @param recipient: A capability to the collection where the new NFT will be deposited
    /// @param recipient: 要将新 NFT 存入的集合的功能
    /// @param name: The name for the NFT metadata
    /// @param name: NFT 元数据的名称
    /// @param description: The description for the NFT metadata
    /// @param description: NFT 元数据的说明
    /// @param thumbnail: The thumbnail for the NFT metadata
    /// @param thumbnail: NFT 元数据的缩略图
    ///
    pub fun mintNFT(
        recipient: &{NonFungibleToken.CollectionPublic},
        name: String,
        description: String,
        thumbnail: String,
    ) {
        let metadata: {String: AnyStruct} = {}

        // this piece of metadata will be used to show embedding rarity into a trait
        // 这段元数据将用于展示如何将稀有度嵌入到性状中
        metadata["foo"] = "bar"

        // create a new NFT
        // 创建一个新的 NFT
        var newNFT <- create NFT(
            id: QuickNFT.totalSupply,
            name: name,
            description: description,
            thumbnail: thumbnail,
            metadata: metadata,
        )

        // deposit it in the recipient's account using their reference
        // 使用其引用将其存入收件人的帐户
        recipient.deposit(token: <-newNFT)

        QuickNFT.totalSupply = QuickNFT.totalSupply + 1
    }

    /// Function that resolves a metadata view for this contract.
    /// 解析此合约的元数据视图的函数。
    ///
    /// @param view: The Type of the desired view.
    /// @param view: 所需视图的类型。
    /// @return A structure representing the requested view.
    /// @return 一个表示所请求视图的结构。
    ///
    pub fun resolveView(_ view: Type): AnyStruct? {
        switch view {
            case Type<MetadataViews.NFTCollectionData>():
                return MetadataViews.NFTCollectionData(
                    storagePath: QuickNFT.CollectionStoragePath,
                    publicPath: QuickNFT.CollectionPublicPath,
                    providerPath: /private/QuickNFTCollection,
                    publicCollection: Type<&QuickNFT.Collection{QuickNFT.QuickNFTCollectionPublic}>(),
                    publicLinkedType: Type<&QuickNFT.Collection{QuickNFT.QuickNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
                    providerLinkedType: Type<&QuickNFT.Collection{QuickNFT.QuickNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
                    createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
                        return <-QuickNFT.createEmptyCollection()
                    })
                )
            case Type<MetadataViews.NFTCollectionDisplay>():
                let media = MetadataViews.Media(
                    file: MetadataViews.HTTPFile(
                        url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg"
                    ),
                    mediaType: "image/svg+xml"
                )
                return MetadataViews.NFTCollectionDisplay(
                    name: "The Example Collection",
                    description: "This collection is used as an example to help you develop your next Flow NFT.",
                    externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"),
                    squareImage: media,
                    bannerImage: media,
                    socials: {
                        "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain")
                    }
                )
        }```markdown
/// 返回由 Non Fungible Token 实现的所有 Metadata Views 的函数
    ///
    /// @return 定义已实现视图的 Type 数组。此值将被
    ///         开发者使用,以了解要传递给 resolveView() 方法的参数。
    ///
    pub fun getViews(): [Type] {
        return [\
            Type<MetadataViews.NFTCollectionData>(),\
            Type<MetadataViews.NFTCollectionDisplay>()\
        ]
    }

    init() {
        // 初始化总供应量
        self.totalSupply = 0

        // 设置命名路径
        self.CollectionStoragePath = /storage/QuickNFTCollection
        self.CollectionPublicPath = /public/QuickNFTCollection

        // 创建一个 Collection 资源并将其保存到存储中
        let collection <- create Collection()
        self.account.save(<-collection, to: self.CollectionStoragePath)

        // 为 collection 创建一个公共能力
        self.account.link<&QuickNFT.Collection{NonFungibleToken.CollectionPublic, QuickNFT.QuickNFTCollectionPublic, MetadataViews.ResolverCollection}>(
            self.CollectionPublicPath,
            target: self.CollectionStoragePath
        )

        emit ContractInitialized()
    }
}

让我们逐步解释智能合约。

导入:

该代码导入了三个接口:NonFungibleTokenViewResolverMetadataViews。这些接口提供了在 Flow 区块链上创建和管理 non-fungible tokens (NFTs) 所需的功能。

由于 Flow 团队已经将这些接口部署在测试网上,我们使用它们的地址来导入这些接口。你可以随时查看 Non-Fungible Token Contract 页面 以查看每个网络的 NonFungibleToken 地址。

当你想将智能合约部署到主网时,你应该相应地更改地址。

合约声明:

此代码定义了一个名为 QuickNFT 的合约,用于创建和管理 NFTs。它声明该合约实现了 NonFungibleTokenViewResolver 接口。

合约状态:

  • totalSupply: 一个变量,用于跟踪现有 QuickNFT 的总数。

  • Events: 该合约定义了几个事件,例如 ContractInitializedWithdrawDeposit,可以发出这些事件以记录重要的合约操作。

存储和路径:

CollectionStoragePathCollectionPublicPath 是用于存储和访问 NFT 集合及其元数据的存储和公共路径。

NFT 资源:

NFT 资源代表一个单独的 NFT,具有各种属性,如 idnamedescriptionthumbnailmetadata。NFTs 使用此资源进行铸造和管理。

NFT 资源中的函数:

  • getViews(): 返回 NFT 支持的元数据视图数组。

  • resolveView(_ view: Type): 解析 NFT 的特定元数据视图。 这些函数创建并返回各种 NFT 方面的元数据视图,例如其显示、版本、外部 URL、集合数据等。

QuickNFTCollectionPublic:

这是一个资源接口,定义了用于从集合中存入、检索和借用 NFT 的方法。

Collection 资源:

Collection 资源代表一个帐户拥有的 NFT 集合。 定义了 depositwithdrawgetIDsborrowNFT 等函数来管理集合中的 NFT。 destroy() 函数用于在不再需要集合时清理集合。

创建集合和铸造 NFTs:

  • createEmptyCollection(): 创建一个空的 NFT 集合。

  • mintNFT(...): 铸造一个新的 NFT 并将其存入接收者的集合中。

合约的元数据视图:

该合约定义了用于解析合约本身的元数据视图的函数,例如 resolveViewgetViews。这些视图提供了有关合约及其集合的信息。

初始化:

init() 函数中,合约初始化其状态,设置存储路径,创建一个集合,并将其链接到公共路径。 它发出一个 ContractInitialized 事件,以指示合约已初始化。

创建脚本

现在,是时候创建脚本了。转到 ./cadence/scripts 目录。

然后,通过运行以下命令在其中创建三个名为 GetIDsQuickNFT.cdcTotalSupplyQuickNFT.cdcGetMetadataQuickNFT.cdc 的文件。

echo > GetIDsQuickNFT.cdc
echo > TotalSupplyQuickNFT.cdc
echo > GetMetadataQuickNFT.cdc

GetIDsQuickNFT

GetIDsQuickNFT 脚本旨在检索与给定地址关联的 NFT 的 ID,利用 MetadataViews 资源和 Flow 区块链提供的功能。

使用你的代码编辑器打开 GetIDsQuickNFT.cdc 文件。复制下面的代码并将其粘贴到文件中。代码解释可以在代码之后立即找到。

import MetadataViews from 0x631e88ae7f1d7c20;

pub fun main(address: Address): [UInt64] {

  let account = getAccount(address)

  let collection = account
    .getCapability(/public/QuickNFTCollection)
    .borrow<&{MetadataViews.ResolverCollection}>()
    ?? panic("Could not borrow a reference to the collection")

  let IDs = collection.getIDs()
  return IDs;
}

这是代码解释:

  • 该脚本从 Flow 区块链上的特定地址导入一个名为 MetadataViews 的接口。

  • 该合约定义了一个名为 main 的函数,该函数接受一个 Address 作为参数。

  • main 函数内部,它使用提供的 Address 来检索与 Flow 区块链上的该地址关联的帐户对象。

  • 然后,它尝试借用对与该帐户关联的 NFT 集合的引用。预计此集合支持 MetadataViews.ResolverCollection 接口。

  • 如果尝试借用对集合的引用失败,它将触发一条消息为“Could not borrow a reference to the collection”的 panic。

  • 假设成功借用了引用,则该函数随后调用集合上的 getIDs() 方法以检索表示集合中 NFT 的 ID 的 UInt64 值数组。

  • 最后,该函数返回此 NFT ID 数组。

TotalSupplyQuickNFT

TotalSupplyQuickNFT 脚本导入 QuickNFT 的智能合约并返回 QuickNFT 的总供应量。

使用你的代码编辑器打开 TotalSupplyQuickNFT.cdc 文件。复制下面的代码并将其粘贴到文件中。

DEPLOYER_ACCOUNT_ADDRESS 替换为你在 创建 Flow 钱包 部分中创建的帐户地址。

import QuickNFT from DEPLOYER_ACCOUNT_ADDRESS

pub fun main(): UInt64 {
    return QuickNFT.totalSupply;
}

GetMetadataQuickNFT

GetMetadataQuickNFT 脚本提供了一种获取有关给定地址拥有的特定 NFT 详细信息的方法,利用 MetadataViews 接口和 Flow 区块链提供的功能。

使用你的代码编辑器打开 GetMetadataQuickNFT.cdc 文件。复制下面的代码并将其粘贴到文件中。代码解释可以在代码之后立即找到。

import MetadataViews from 0x631e88ae7f1d7c20;

pub fun main(address: Address, id: UInt64): NFTResult {

  let account = getAccount(address)

  let collection = account
      .getCapability(/public/QuickNFTCollection)
      .borrow<&{MetadataViews.ResolverCollection}>()
      ?? panic("Could not borrow a reference to the collection")

  let nft = collection.borrowViewResolver(id: id)

  var data = NFTResult()

  // 获取此 NFT 的基本显示信息
  if let view = nft.resolveView(Type<MetadataViews.Display>()) {
    let display = view as! MetadataViews.Display

    data.name = display.name
    data.description = display.description
    data.thumbnail = display.thumbnail.uri()
  }

  // 所有者直接存储在 NFT 对象上
  let owner: Address = nft.owner!.address

  data.owner = owner
  data.id = id

  return data
}

pub struct NFTResult {
  pub(set) var name: String
  pub(set) var description: String
  pub(set) var thumbnail: String
  pub(set) var owner: Address
  pub(set) var id: UInt64

  init() {
    self.name = ""
    self.description = ""
    self.thumbnail = ""
    self.owner = 0x0
    self.id = 0
  }
}

这是代码解释:

  • 该脚本首先从 Flow 区块链上的特定地址导入一个名为 MetadataViews 的资源。

  • main 函数接受两个参数:一个 Address,表示所有者的地址,以及一个 UInt64 ID,它标识感兴趣的 NFT。

  • main 函数内部,它检索与 Flow 区块链上提供的地址关联的帐户。

  • 然后,它尝试借用对与该帐户关联的 NFT 集合的引用。预计此集合支持 MetadataViews.ResolverCollection 接口。如果借用尝试失败,它会触发一条消息为“Could not borrow a reference to the collection”的 panic。

  • 成功借用对集合的引用后,该函数调用集合上的 borrowViewResolver 以检索对给定 ID 标识的特定 NFT 的引用。

  • 该函数初始化一个名为 dataNFTResult 类型的变量,以存储有关 NFT 的信息。

  • 它首先检索 NFT 的基本显示信息。如果 NFT 支持 MetadataViews.Display 视图,它会从视图中提取名称、描述和缩略图 URI,并将它们存储在数据结构中。

  • 最后,该函数使用所有者的地址、NFT ID 和其他详细信息填充 data 结构,然后将其作为结果返回。

  • 该脚本还定义了一个 NFTResult 结构,其中包括要返回的有关 NFT 的信息。它包括名称、描述、缩略图 URI、所有者的地址和 NFT 的 ID 的字段。

创建交易

转到 ./cadence/transactions 目录。

然后,通过运行以下命令在其中创建两个名为 SetUpAccount.cdcMintNFT.cdc 的文件。

echo > SetUpAccount.cdc
echo > MintNFT.cdc

SetUpAccount

此文件是一个 Flow 区块链交易,旨在在用户的帐户上创建一个新的空 QuickNFT 集合,以便能够铸造 QuickNFT。

使用你的代码编辑器打开 SetUpAccount.cdc 文件。复制下面的代码并将其粘贴到文件中。代码解释可以在代码之后立即找到。

DEPLOYER_ACCOUNT_ADDRESS 替换为你在 创建 Flow 钱包 部分中创建的帐户地址。

注意

此交易文件需要从你的帐户地址导入 QuickNFT 智能合约。你已经创建了你的帐户,并且你拥有你的帐户地址。但是,我们尚未涵盖部署主题,并且你的 QuickNFT 智能合约尚未部署在测试网上。

由于 QuickNFT 智能合约将部署在你的帐户上,因此它将按预期工作。

import NonFungibleToken from 0x631e88ae7f1d7c20
import MetadataViews from 0x631e88ae7f1d7c20
import QuickNFT from DEPLOYER_ACCOUNT_ADDRESS

transaction {

    prepare(signer: AuthAccount) {
        // 如果帐户已经有一个集合,则提前返回
        if signer.borrow<&QuickNFT.Collection>(from: QuickNFT.CollectionStoragePath) != nil {
            return
        }

        // 创建一个新的空集合
        let collection <- QuickNFT.createEmptyCollection()

        // 将其保存到帐户
        signer.save(<-collection, to: QuickNFT.CollectionStoragePath)

        // 为集合创建一个公共能力
        signer.link<&{NonFungibleToken.CollectionPublic, QuickNFT.QuickNFTCollectionPublic, MetadataViews.ResolverCollection}>(
            QuickNFT.CollectionPublicPath,
            target: QuickNFT.CollectionStoragePath
        )
    }
}

这是代码解释:

  • 它导入了两个接口 NonFungibleTokenMetadataViews,以及我们构建的智能合约 QuickNFT

  • 事务在 transaction 块中定义,表明它包含 prepareexecute 阶段。

  • prepare 阶段(在提交事务之前执行),合约执行以下操作:

    • 它检查帐户(由 signer 表示)是否已经有一个 NFT 集合。如果确实有,则事务会提前返回,表明不需要进一步的设置。

    • 如果帐户没有集合,它会使用 QuickNFT.createEmptyCollection() 函数创建一个新的空 NFT 集合。

    • 然后使用 signer.save() 将新创建的集合保存到帐户。

    • 它还会为集合创建一个公共能力,使其他人能够与之交互。

MintNFT

此文件是一个 Flow 区块链事务,旨在铸造一个新的 QuickNFT 并将其存入指定接收者的集合中。

使用你的代码编辑器打开 MintNFT.cdc 文件。复制下面的代码并将其粘贴到文件中。代码解释可以在代码之后立即找到。

DEPLOYER_ACCOUNT_ADDRESS 替换为你在 创建 Flow 钱包 部分中创建的帐户地址。

import NonFungibleToken from 0x631e88ae7f1d7c20
import MetadataViews from 0x631e88ae7f1d7c20
import QuickNFT from DEPLOYER_ACCOUNT_ADDRESS

transaction(
    recipient: Address,
    name: String,
    description: String,
    thumbnail: String,
) {

    /// 接收者集合的引用
    let recipientCollectionRef: &{NonFungibleToken.CollectionPublic}

    /// 事务执行之前的上一个 NFT ID
    let mintingIDBefore: UInt64

    prepare(signer: AuthAccount) {
        self.mintingIDBefore = QuickNFT.totalSupply

        // 借用接收者的公共 NFT 集合引用
        self.recipientCollectionRef = getAccount(recipient)
            .getCapability(QuickNFT.CollectionPublicPath)
            .borrow<&{NonFungibleToken.CollectionPublic}>()
            ?? panic("Could not get receiver reference to the NFT Collection")
    }

    execute {

        // 铸造 NFT 并将其存入接收者的集合中
        QuickNFT.mintNFT(
            recipient: self.recipientCollectionRef,
            name: name,
            description: description,
            thumbnail: thumbnail,
        )
    }

    post {
        self.recipientCollectionRef.getIDs().contains(self.mintingIDBefore): "The next NFT ID should have been minted and delivered"
        QuickNFT.totalSupply == self.mintingIDBefore + 1: "The total supply should have been increased by 1"
    }
}

这是代码解释:

  • 它导入了两个接口 NonFungibleTokenMetadataViews,以及我们构建的智能合约 QuickNFT

  • 它定义了一个事务,该事务采用多个参数,包括接收者(NFT 接收者的地址)、name、description 和 thumbnail。

  • 在事务中,有三个主要部分:prepareexecutepost

  • prepare 阶段(在确认事务之前执行),合约执行以下任务:

    • 它将 NFT 的当前总供应量存储在 mintingIDBefore 变量中,该变量表示事务执行之前的 NFT ID。

    • 它尝试使用接收者的地址借用对接收者的公共 NFT 集合的引用。

    • 如果借用集合引用失败,它会触发一个 panic,表明它无法检索引用。

  • execute 阶段(在确认事务时执行),合约使用 QuickNFT.mintNFT 函数铸造一个新的 NFT。此函数将新铸造的 NFT 存入接收者的集合中,并包括提供的元数据(name、description 和 thumbnail)。

  • post 阶段(在 execute 阶段之后执行),合约验证两个条件:

    • 它检查 NFT ID 是否已递增 1(表明已铸造新的 NFT 并已将其交付给接收者)。

    • 它确保 NFT 的总供应量已增加 1。

现在,所有与智能合约相关的文件都已完成。

准备部署

正如你所记得的那样,我们解释了 flow.json 文件及其属性。现在,是时候更新该文件以定义将要在项目中使用的所有合约了。

打开 flow.json 文件。我们将进行一些更改。

首先,像下面这样修改 contracts 对象。如果你使用不同的合约名称,请相应地更改它。

"contracts": {
    "QuickNFT": "cadence/contracts/QuickNFT.cdc"
  }

然后,像下面这样修改 deployments 对象。它基本上说,“通过 testnet-accounttestnet 上部署 QuickNFT 合约”。

"deployments": {
    "testnet": {
      "testnet-account": ["QuickNFT"]
    }
  }

注意

如果你的配置文件到目前为止没有 deployments 对象,你可以在其他对象(例如 accounts)之后添加它。随时查看下面的文件最终版本。

在本指南中,无需修改 networksaccounts 对象。但是,如果你想使用你的自定义 QuickNode 端点而不是公共端点,则应修改 networks 对象。

总而言之,flow.json 文件的最终版本应如下所示。当然,你在 testnet-account 中的地址会有所不同。

{
  "contracts": {
    "QuickNFT": "cadence/contracts/QuickNFT.cdc"
  },
  "networks": {
    "emulator": "127.0.0.1:3569",
    "mainnet": "access.mainnet.nodes.onflow.org:9000",
    "testnet": "access.devnet.nodes.onflow.org:9000"
  },
  "accounts": {
    "emulator-account": {
      "address": "f8d6e0586b0a20c7",
      "key": {
        "type": "file",
        "location": "./emulator.key"
      }
    },
    "testnet-account": {
      "address": "bcc2fbf2808c44b6",
      "key": {
        "type": "file",
        "location": "testnet-account.pkey"
      }
    }
  },
  "deployments": {
    "testnet": {
      "testnet-account": ["QuickNFT"]
    }
  }
}

多亏了 scaffold 功能,一开始就创建了构建简单去中心化应用程序的所有必要文件和文件夹。现在,是时候将智能合约部署到测试网并立即运行 dApp 了。

请确保你的终端指向项目的根目录。

首先,运行命令。

npm install

然后,运行以下命令。此命令将智能合约部署到测试网并在本地运行 Web 应用程序。

请注意,它会继续在本地运行项目,直到你结束该命令。我们不会结束该命令以立即查看我们所做更新的效果。

npm run dev:testnet:deploy

控制台输出应类似于以下内容。

> fcl-next-scaffold@0.3.1 dev:testnet:deploy
> flow project deploy --network=testnet --update && cross-env NEXT_PUBLIC_FLOW_NETWORK=testnet next dev

Deploying 1 contracts for accounts: testnet-account

QuickNFT -> 0xbcc2fbf2808c44b6 (1976e0c6fcfba3349a258073dc8b2c626175c3c8ee761c51d300bd66abab7086)

🎉 All contracts deployed successfully

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 227 ms (198 modules)

你可以在 Flow 区块浏览器上检查你的帐户余额、智能合约和事务。

现在,使用你的浏览器打开 http://localhost:3000。该网站应如下所示。

Web Application

你可能会注意到,我们尚未对 Web 应用程序执行任何操作。但是,由于我们选择的 scaffold([5] FCL Web Dapp),基本 Web 应用程序已准备就绪。

现在,让我们跳到前端并修改应用程序,使其成为 NFT 集合 dapp。

构建前端

由于在应用程序的开发中使用了 Next.js,因此我们的组件已经在 components 文件夹中,与样式相关的文件在 styles 文件夹中,并且主页的文件位于 ./pages/index.tsx 中。我们的修改将在此这些文件夹中。

主页

打开 ./pages/index.tsx 以修改主页。

将以下代码替换为现有代码。有关详细信息,请参见代码中的注释。

总而言之,此文件代表一个主页,该主页显示有关 Flow 区块链上“QuickNFT Collection”的信息。它包括基于用户身份验证状态的条件渲染,并为主页设置元数据。

// 导入必要的模块和组件
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Links from "../components/Links";
import Container from "../components/Container";
import useCurrentUser from "../hooks/useCurrentUser";

export default function Home() {
  // 使用 useCurrentUser hook 检查用户是否已登录
  const { loggedIn } = useCurrentUser();

  // 返回网页的 JSX 结构
  return (
    <div className={styles.container}>
      <Head>
        <title>QuickNFT on Flow</title>
        <meta name="description" content="QuickNFT Collection" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      {/* 主要内容部分 */}
      <main className={styles.main}>
        <h1 className={styles.title}>QuickNFT Collection</h1>

        <p className={styles.description}>For the Flow Blockchain</p>

        {/* 如果用户已登录,则有条件地渲染 Container 组件 */}
        {loggedIn && <Container />}

        {/* 渲染 Links 组件 */}
        <Links />
      </main>
    </div>
  );
}

Container 组件

Container 组件是一个 React 组件,负责管理 NFT 集合应用程序的各个方面。它包括从区块链获取用户拥有的 NFT 的函数、使用随机元数据铸造新的 NFT 的函数以及设置帐户以接收 NFT 的函数。此外,它还显示用户铸造的 NFT 总数、提供事务链接供用户监视他们在区块链上的操作,并以缩略图和描述直观地展示用户的 NFT 集合。

NFT 的元数据在代码中定义为 nftMetadata。如果要更改其元数据(例如缩略图),请随时编辑 nftMetadata。如果你有兴趣使用 IPFS 存储你的缩略图,请查看 我们的指南

打开 ./components/Container.tsx 以修改 Container 组件。

将以下代码替换为现有代码。有关详细信息,请参见代码中的注释。

你可能会注意到,前一节中定义的脚本和事务在此组件中使用。

// 导入必要的模块和组件
import * as fcl from "@onflow/fcl"; // 用于与区块链交互的 Flow 客户端库
import * as types from "@onflow/types"; // 用于参数类型的 Flow 类型
import useCurrentUser from "../hooks/useCurrentUser"; // 用于用户身份验证的自定义 hook
import { useEffect, useState } from "react"; // 用于管理状态的 React hooks
import TotalSupplyQuickNFT from "../cadence/scripts/TotalSupplyQuickNFT.cdc"; // 用于获取 NFT 总供应量的 Cadence 脚本
import GetMetadataQuickNFT from "../cadence/scripts/GetMetadataQuickNFT.cdc"; // 用于获取 NFT 元数据的 Cadence 脚本
import GetIDsQuickNFT from "../cadence/scripts/GetIDsQuickNFT.cdc"; // 用于获取 NFT ID 的 Cadence 脚本
import SetUpAccount from "../cadence/transactions/SetUpAccount.cdc"; // 用于设置用户帐户的 Cadence 事务
import MintNFT from "../cadence/transactions/MintNFT.cdc"; // 用于铸造 NFT 的 Cadence 事务
import elementStyles from "../styles/Elements.module.css"; // 元素的 CSS 样式
import containerStyles from "../styles/Container.module.css"; // 容器的 CSS 样式
import useConfig from "../hooks/useConfig"; // 用于配置的自定义 hook
import { createExplorerTransactionLink } from "../helpers/links"; // 用于创建事务链接的辅助函数

// 生成 0 到 2 之间的随机整数的函数
function randomInteger0To2(): number {
  return Math.floor(Math.random() * 3);
}

// 将 Container 组件定义为默认导出
export default function Container() {
  // 状态变量,用于存储数据和事务信息
  const [totalSupply, setTotalSupply] = useState(0);
  const [datas, setDatas] = useState([]);
  const [txMessage, setTxMessage] = useState("");
  const [txLink, setTxLink] = useState("");

  // 用于获取网络配置的自定义 hook
  const { network } = useConfig();

  // 用于获取用户身份验证状态的自定义 hook
  const user = useCurrentUser();

  // 用于查询区块链以获取 NFT 总供应量的函数
  const queryChain = async () => {
    const res = await fcl.query({
      cadence: TotalSupplyQuickNFT,
    });

    setTotalSupply(res);
  };

  // 用于处理用户帐户设置以接收 NFT 的函数
  const mutateSetUpAccount = async (event) => {
    event.preventDefault();

    // 重置与事务相关的状态
    setTxLink("");
    setTxMessage("");

    // 在区块链上执行 setUpAccount 事务
    const transactionId = await fcl.mutate({
      cadence: SetUpAccount,
    });

    // 生成一个事务链接供用户检查事务状态
    const txLink = createExplorerTransactionLink({ network, transactionId });

    // 更新与事务相关的状态以通知用户
    setTxLink(txLink);
    setTxMessage("检查你的设置事务。");
  };

  // 用于处理新 NFT 铸造的函数
  const mutateMintNFT = async (event) => {
    event.preventDefault();

    // 重置与事务相关的状态
    setTxLink("");
    setTxMessage("");

    // 生成一个随机整数以选择 NFT 元数据
    const rand: number = randomInteger0To2();

    // 定义一个预定义的 NFT 元数据数组
    const nftMetadata = [\
      {\
        name: "Quick NFT",\
        description: "Original QNFT",\
        thumbnail: "ipfs://QmYXV94RimuC3ubtyEHptTHLbh86cSRNtPuscfXmJ9jmmc",\
      },\
      {\
        name: "Quick NFT",\
        description: "Grainy QNFT",\
        thumbnail: "ipfs://QmYRvjpozSu8JE1jfnWDYXyT8VWVVYqsDUjuUwXwzPLwdq",\
      },\
      {\
        name: "Quick NFT",\
        description: "Faded QNFT",\
        thumbnail: "ipfs://QmSiswWjzwPwyW1eJvHQfd9E98DjHXovWXTggYdbFKKv8J",\
      },\
    ];

    // 在区块链上执行 mintNFT 事务
    const transactionId = await fcl.mutate({
      cadence: MintNFT,
      args: (arg, t) => [\
        arg(user.addr, types.Address),\
        arg(nftMetadata[rand].name, types.String),\
        arg(nftMetadata[rand].description, types.String),\
        arg(nftMetadata[rand].thumbnail, types.String),\
      ],
    });

    // 生成一个事务链接供用户检查事务状态
    const txLink = createExplorerTransactionLink({
      network,
      transactionId,
    });

    // 更新与事务相关的状态以通知用户
    setTxLink(txLink);
    setTxMessage("检查你的 NFT 铸造事务。");

    // 获取更新后的用户 NFT 列表
    await fetchNFTs();
  };

  // 用于获取用户 NFT 的函数
  const fetchNFTs = async () => {
    // 将 datas 状态重置为一个空数组
    setDatas([]);
    // 初始化一个数组以存储 NFT ID
    let IDs = [];

    try {
      // 查询区块链以获取用户拥有的 NFT 的 ID
      IDs = await fcl.query({
        cadence: GetIDsQuickNFT,
        args: (arg, t) => [arg(user.addr, types.Address)],
      });
    } catch (err) {
      console.log("No NFTs Owned");
    }

    // 初始化一个数组以存储 NFT 元数据
    let _src = [];

    try {
      // 迭代每个 NFT ID 并从区块链中获取元数据
      for (let i = 0; i < IDs.length; i++) {
        const result = await fcl.query({
          cadence: GetMetadataQuickNFT,
          args: (arg, t) => [\
            arg(user.addr, types.Address),\
            arg(IDs[i].toString(), types.UInt64),\
          ],
        });

        // 处理缩略图 URL 为 IPFS URL 的情况
        let imageSrc = result["thumbnail"];
        if (result["thumbnail"].startsWith("ipfs://")) {
          imageSrc =
            "https://quicknode.myfilebase.com/ipfs/" + imageSrc.substring(7);
        }

        // 将 NFT 元数据添加到 _src 数组
        _src.push({
          imageUrl: imageSrc,
          description: result["description"],
          id: result["id"],
        });
      }

      // 使用获取的 NFT 元数据更新 datas 状态
      setDatas(_src);
    } catch (err) {
      console.log(err);
    }
  };

  // Effect hook, used to fetch user's NFTs when the user is authenticated
  useEffect(() => {
    if (user && user.addr) {
      fetchNFTs();
    }
  }, [user]);
``````markdown
return (
    <div className={containerStyles.container}>
      <div>
        <button onClick={queryChain} className={elementStyles.button}>
          查询总供应量
        </button>
        <h4>铸造的 NFT 总数:{totalSupply}</h4>
      </div>
      <hr />
      <div>
        <h2>铸造你的 NFT</h2>
        <div>
          <button onClick={mutateSetUpAccount} className={elementStyles.button}>
            设置账户
          </button>

          <button onClick={mutateMintNFT} className={elementStyles.button}>
            铸造 NFT
          </button>
        </div>
        <div>
          {txMessage && (
            <div className={elementStyles.link}>
              <a href={txLink} target="_blank" rel="noopener noreferrer">
                {txMessage}
              </a>
            </div>
          )}
        </div>
      </div>
      <hr />
      <div>
        <h2>你的 NFTs</h2>
        <div className={containerStyles.nftcontainer}>
          {datas.map((item, index) => (
            <div className={containerStyles.nft} key={index}>
              <img src={item.imageUrl} alt={"NFT 缩略图"} />
              <p>{`${item.description} #${item.id}`}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

容器样式

Container.module.css 文件用于容器的 CSS 样式。我们希望修改此文件,以图库视图显示连接账户拥有的 NFT。

打开 ./styles/Container.module.css

将以下代码替换为现有代码。有关详细信息,请参见代码中的注释。

.container {
  text-align: center;
  padding: 20px 0 50px 0;
}

.nftcontainer {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  margin: -10px;
  /* Negative margin to offset the padding on child items */
  /* 负边距来抵消子项目的内边距 */
}

/* Style each NFT item */
/* 样式化每个 NFT 项目 */
.nft {
  width: calc(20% - 20px);
  /* Adjust the width as needed for your layout */
  /* 根据你的布局需要调整宽度 */
  margin: 10px;
  padding: 10px;
  border: 1px solid #ddd;
  text-align: center;
}

.nft img {
  max-width: 100%;
  height: auto;
}

.nft p {
  margin-top: 10px;
}

在前端铸造 NFTs

现在,让我们尝试铸造 NFTs。

再次转到浏览器上的应用程序(http://localhost:3000)。

点击 Log In With Wallet(使用钱包登录),然后选择你的 Flow 钱包。如果使用 Lilico,在设置你的钱包之后,你应该使用你的公钥在此处创建你的 Flow 账户。在本指南中,我们使用 Blocto 以方便使用,因为它只需要一个电子邮件地址即可创建账户。

在你将你的钱包连接到网站后,应该看到如下所示。

主页

首先,正如我们在智能合约开发期间提到的那样,用户应该设置他们的账户才能够铸造 NFTs。

点击 Set Up Account(设置账户),它会弹出一个 Confirm Transaction(确认交易)页面。然后,点击 Approve(批准)。在将交易发送到区块链后,你可以通过点击 Check your setup transaction(检查你的设置交易)消息来检查你的交易。

设置账户交易

然后,点击 Mint NFT(铸造 NFT)并批准交易。如果一切按预期进行,你的 NFT 应该会显示在 Your NFTs(你的 NFTs)部分下。

此外,如果你点击 Query Total Supply(查询总供应量),则会从区块链获取该集合的总铸造 NFT 数量并显示出来。

铸造 NFT

恭喜!你刚刚在 Flow Testnet 上创建了一个 NFT 集合,并通过前端的 Flow 钱包成功铸造了一个 NFT。

目前,我们已经完成了开发方面的工作。现在,让我们来看看为什么 Flow 是一个适合 NFT 项目的区块链。

Cadence 如何实现组合以及它与 EIP-6551 的不同之处

重新审视 Cadence 中的所有权

希望通过本指南前面的内容,你现在应该理解,Flow 中的 NFT 只是实现 Non-Fungible Token(非同质化代币)标准的资源。Cadence 中的资源是类型化对象的实例,并且由于 Cadence 是一种面向对象的语言,这意味着你可以将资源存储在另一个资源中。

在其他区块链生态系统中,所有权是通过将账户地址标识符映射到资产的 ID 来处理的,在用户或应用程序不拥有的特定代币合约中处理。相比之下,Flow 通过使开发人员能够显式地声明资源之间的直接关系,从而将所有权提升到一个更深层次和更直观的级别。这实现了真正的封装,将逻辑和数据组合在一个给定的资源中,并允许开发人员在组织代码时应用领域驱动设计原则。

Cadence 中可能的面向对象的封装彻底改变了链上复杂应用程序的可能性。内部化在资源中的逻辑,以及通过功能控制对该逻辑的访问,将产生无法被利用的良好安全解决方案。这提供的可移植性也非常强大。如果内部存储其他 NFT(B 和 C)的 NFT A 被发送到另一个账户,则接收用户可以预期 A 在收到时仍然包含 NFT B 和 C。嵌套资源的整个图以原子方式从发送者账户转移到接收者,而 B 和 C 在 A 中的存储方式没有任何改变。在任何时候,交易都不需要引用 B 或 C 才能实现这一点,通过交易发送的唯一 NFT 是 A。

Flow vs EIP-6551

在实践层面,EIP-6551 是一个框架,使 EVM 链上的 ERC-721 代币能够“存储” NFT 内的其他 NFT。重要的是,它是 Solidity 的扩展(因此是 EIP),旨在标准化 NFT 拥有其他 NFT 的方式。该提案没有对 Solidity 语言进行任何更改,保证所有权关系的工作由 EIP-6551 框架管理。

与 Cadence 的主要区别在于,所有权和封装直接内置到语法中,这是其面向对象语言设计的结果。这是许多开发人员都熟悉的一种编程模型。Cadence 通过原生构建解决方案,直接解决了传统的区块链工程的区块链用例和挑战。这样做大大简化了开发人员在构建时需要关注的内容。

Flow 和 EIP-6551 之间的主要区别

ERC-6551 采用了一种无需许可的注册表,增强了与现有 ERC-721 NFT 的兼容性。该注册表既充当 Token-Bound Accounts(TBAs,代币绑定账户)的工厂又充当目录,允许任何人通过简单地调用注册表函数并支付少量费用来为 ERC-721 代币创建 TBA。生成的代理合约继承了原始 ERC-721 代币的所有属性和元数据,使其能够与各种智能合约交互,甚至可以持有其他资产。上面提到的“交互”实际上是由 NFT 链接到的 TBA 促成的,而不是 NFT 本身。

值得注意的是,希望使用该框架的开发人员必须自己部署和运营注册表系统,作为其应用程序逻辑的额外工程工作。

Flow 的方法和 EIP-6551 之间的主要区别详述如下:

  • 与直接在 Cadence 中编写你的 OO(面向对象)模型相比,EIP-6551 的实现更为复杂,使用起来也不太直观。
  • EIP-6551 模拟了一定程度的对象组合,以解决 Solidity 缺乏原生支持的问题。对于可能的组合类型或复杂性,可能存在实际限制,例如:嵌套对象图,使用 EIP-6551 可以实现。
  • EIP-6551 仍处于提案阶段,仍在开发中。这意味着现有 NFT 项目和链的支持有限。Cadence 自 2020 年 10 月以来一直在 Flow 主网上运行。
  • EIP-6551 的复杂性为应用程序增加了相当大的攻击面,这反过来可能会导致安全问题。
  • Cadence 的设计意味着每个资源都可以唯一地定义自己的安全模型,并同时控制必要的功能。随着应用程序变得越来越复杂,这一点变得越来越重要。
  • Cadence 中的 NFT 始终存储在一个账户中。但是,它们并不是与“自己拥有的”账户内在联系的;只有在 EIP-6551 中才需要这种结构才能使解决方案发挥作用。如果需要,Cadence 中的资源可以内部化 AuthAccount 对象,尽管此高级主题超出了本摘要的范围。
  • 语言保证了嵌套资源的可移植性。鉴于 EIP-6551 的设计方式,需要为从所有者 NFT 批量传输拥有的 NFT 编写代码

结论

就这样!至此,你应该对 Flow 的独特功能(例如面向资源的架构和 资源拥有资源 概念)有清楚的了解,以及如何使用 Cadence 和 Flow CLI 在 Flow 上创建 NFT 集合去中心化应用程序。

总而言之,本指南解释了 Flow 及其编程语言 Cadence 的关键功能,并帮助你利用 Flow 的特殊功能在 Flow 上创建自己的 NFT 集合 dApp。有了这些知识,你就可以加入数字资产和去中心化应用程序的世界,在区块链领域将你的创意变为现实。

如果你遇到任何问题或有任何疑问,我们很乐意为你提供帮助!在 DiscordTwitter 上找到我们。

Flow 生态系统

查看以下链接以获取有关 Flow 的更多信息:

我们 ❤️ 反馈!

如果你对新主题有任何反馈或要求,请告诉我们。我们很乐意听取你的意见。



>- 原文链接: [quicknode.com/guides/flo...](https://www.quicknode.com/guides/flow/how-to-create-an-nft-collection-on-flow)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。