如何使用Cadence在Flow上创建NFT收藏品dApp - Quicknode

  • QuickNode
  • 发布于 2025-02-12 16:31
  • 阅读 33

本文详细介绍了Flow区块链及其智能合约语言Cadence,重点展示了如何创建NFT集合并构建前端应用。文章内容涵盖了Flow的多角色架构、资源导向编程、帐户概念以及可升级性等特点,提供了清晰的步骤与代码示例,帮助开发者在Flow测试网络上进行NFT的创建与管理。最后,文章还探讨了Cadence与EIP-6551的比较,阐明了其在区块链开发中的优势。

概述

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

在本指南中,你将了解 Flow。在这里,你将发现如何使用 Cadence、Flow CLI 和 NextJS 创建 NFT 收集并构建前端。

虽然我们的探索之旅发生在 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 允许开发者定义自己的资源类型并强制执行自定义使用规则。Cadence 的设计降低了在其他智能合约语言中编程错误和漏洞的风险,因为资源不能被复制或销毁,只能存储在账户中。在代码执行期间,如果资源在完成时未存储在账户中,函数将中止。

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

账户概念

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 以可替代资产(Fungible-Token Resources)出售,以点对点的方式处理——资产直接存入接收方,而不是通过中央合约进行交易。

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

基于能力的访问控制: Cadence 使用能力限制对保护功能或对象的访问。能力是一种类似钥匙的实体,事先提供给持有账户,只允许他们发起特定操作。能力可以直接提供给特定账户或在需要时被撤销。

非同质化代币标准

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

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

2. Collection - 存放同类型多个 NFT 的资源: 集合是可以存储同类型多个 NFT 的仓库。用户通常对每种 NFT 类型维护一个集合,这个集合存储在其账户的预定义位置。

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

强大的 NFT 生态系统

Flow 已经成为一条面向主流的区块链,并因其体育 NFT 项目而闻名,如 NBA TopShotNFL AllDayUFC Strike,允许用户拥有和交易来自他们最喜欢的体育的各种稀有度的数字时刻。许多其他 NFT 项目也在 Flow 上启动,包括 DoodlesFlovatar 以及 许多其他项目

开发者设置

通过 QuickNode 访问 Flow

要在 Flow 上构建,你需要一个连接 Flow 网络的端点。你可以使用由 QuickNode 提供支持的 Flow 公共端点,或部署和管理自己的基础设施;但是,如果你希望更快的响应时间,可以让我们来承担重担。查看为什么 Flow 选择 QuickNode 作为其公共节点提供商并在 这里 注册一个免费帐户。

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

设置你的开发环境

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

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

创建 Flow 项目

在你的终端中运行以下代码以设置项目。我们的项目文件夹名称将是 nft-collection-qn,但你可以根据需要修改名称。--scaffold 标志用于使用提供的脚手架,这是一个可以用于启动开发的项目模板。

flow setup nft-collection-qn --scaffold

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

Enter the scaffold number: 5

🎉 恭喜!你的项目已创建。

请按以下步骤开始开发:
1. 'cd nft-collection-qn' 以更改为新项目,
2. 'flow emulator' 或运行 Flowser 来启动模拟器,
3. 'flow dev' 开始开发。

你还应该阅读 README.md 以了解更多有关开发过程的信息!

运行以下命令以更改你的目录。

cd nft-collection-qn

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

配置你的 Flow 账户

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

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

flow accounts create

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

控制台输出应如下所示。

🎉 在 Testnet 网络上创建了新的账户,地址为 0xbcc2fbf2808c44b6,名称为 testnet-account。

以下是所有执行的操作的总结:
 - 将新账户添加到 flow.json。
 - 将私钥保存到 testnet-account.pkey。
 - 将 testnet-account.pkey 添加到 .gitignore。

请保存账户地址,因为在后续部分中将需要使用它。

正如输出中提到的,这个命令执行了以下操作:

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

经过这个步骤后,你的账户已创建并已提供资金。

检查配置文件

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

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

文件的默认状态可能没有上述所有属性。这并不是问题;当我们准备进行部署时,将逐步更新该文件。

当我们之前使用 flow accounts create 命令创建钱包时,我们的账户已自动添加到这个配置文件中。但是,如果你有钱包凭据要导入,请在这里添加它们。(有关详细信息,请参见 Flow CLI 配置 文档。)

目前不需要更新此文件,但我们强烈建议你查看该文件以更好地理解配置文件。

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

├── README.md              # 你应用程序的文档。
├── cadence                # Cadence 语言文件(智能合约、交易和脚本)。
├── components             # 用于你应用的 React 组件。
├── config                 # 你应用的配置文件。
├── constants              # 你应用中使用的常量。
├── emulator.key           # 模拟器密钥文件(用于本地开发)。
├── flow.json              # Flow 区块链配置或设置。
├── helpers                # 用于你应用的辅助函数或工具。
├── hooks                  # 用于你应用的自定义 React hooks。
├── jest.config.js         # Jest 测试框架的配置文件。
├── layouts                # 你应用的布局组件。
├── next.config.js         # Next.js 的配置文件。
├── package-lock.json      # 自动生成的文件以锁定依赖版本。
├── package.json           # Node.js 的包配置文件。
├── pages                  # Next.js 的路由页面。
├── public                 # Next.js 提供的静态文件。
├── styles                 # 你应用的样式表和 CSS。
├── testnet-account.pkey   # 测试网账户私钥(用于测试目的)。
├── tsconfig.json          # 你应用的 TypeScript 配置。
└── types                  # 你应用的 TypeScript 类型声明。

在 Flow 上创建 NFT 集合

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

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

信息

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

交易:交易是包含一组更新 Flow 状态的指令的加密签名数据消息。它们是计算的基本单元,由执行节点执行。为了使交易纳入 Flow 区块链,付款人需要支付费用。

不过,在跳入编码之前,让我们进一步了解 Flow 的原生 NFT 标准。

Flow 的原生 NFT 标准

Flow 的原生 NFT 标准,通常称为 NonFungibleToken 合约,定义了一组基本功能,以确保 NFTs 的安全和高效管理。该标准为创建、交易和与 NFTs 交互提供了简化的途径,强调 Flow 对简洁性和用户友好开发的承诺。

NonFungibleToken 合约的核心特性包含两个基本资源接口:NFT(描述单个 NFT 的结构)和 Collection(用于存放同类型多个 NFT)。用户通常通过在其账户存储中的预定义位置保存每种 NFT 类型的集合来组织他们的 NFT。例如,用户可能将其所有的 NBA Top Shot Moments 存储在位于 /storage/MomentCollection 的 TopShot.Collection 中。

要创建新的 NFT 集合,开发者可以利用 createEmptyCollection 函数,该函数生成一个空集合,不包含任何 NFT。用户通常将这些新集合保存到其账户中的一个可识别位置,并利用 NonFungibleToken.CollectionPublic 接口建立一个公共能力以用于连接。

在将 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 {

    /// 总共存在的 QuickNFT 的总供应量。
    pub var totalSupply: UInt64

    /// 合约创建时发出的事件。
    pub event ContractInitialized()

    /// 从集合中提取 NFT 时发出的事件。
    pub event Withdraw(id: UInt64, from: Address?)

    /// 存入集合时发出的事件。
    pub event Deposit(id: UInt64, to: Address?)

    /// 存储和公共路径
    pub let CollectionStoragePath: StoragePath
    pub let CollectionPublicPath: PublicPath

    /// 代表非同质化代币的核心资源。
    /// 新实例将使用 NFTMinter 资源创建,
    /// 并存储在集合资源中。
    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {

        /// 每个 NFT 具有的独特 ID。
        pub let id: UInt64

        /// 元数据字段。
        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
        }

        /// 返回 NFT 实现的所有元数据视图的函数。
        ///
        /// @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>()\
            ]
        }

        /// 解析此Token的元数据视图的函数。
        ///
        /// @param 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>():
                    // 从此合约铸造的 NFT 没有最大数量
                    // 所以最大版次字段的值设为 nil
                    let editionInfo = MetadataViews.Edition(name: "示例 NFT 版次", 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: "示例集合",
                        description: "此集合用作示例,以帮助你开发下一个 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>():
                    // 排除 mintedTime 和 foo 以展示 Traits 的其他用途
                    let excludedTraits = ["mintedTime", "foo"]
                    let traitsView = MetadataViews.dictToTraits(dict: self.metadata, excludedNames: excludedTraits)

                    // mintedTime 为 Unix 时间戳,我们应该标记它以便显示类型,以便平台知道如何展示它。
                    let mintedTimeTrait = MetadataViews.Trait(name: "mintedTime", value: self.metadata["mintedTime"]!, displayType: "Date", rarity: nil)
                    traitsView.addTrait(mintedTimeTrait)

                    // foo 是一个具有自己稀有度的特征
                    let fooTraitRarity = MetadataViews.Rarity(score: 10.0, max: 100.0, description: "常见")
                    let fooTrait = MetadataViews.Trait(name: "foo", value: self.metadata["foo"], displayType: nil, rarity: fooTraitRarity)
                    traitsView.addTrait(fooTrait)

                    return traitsView

            }
            return nil
        }
    }

    /// 定义此 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):
                    "无法借用 QuickNFT 引用:返回引用的 ID 不正确"
            }
        }
    }

    /// 存放 NFTs 的资源。
    /// 为了能够管理 NFTs,任何账户都需要先创建一个空集合
    ///
    pub resource Collection: QuickNFTCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
        // NFT符合的资源类型的字典
        // NFT是一个资源类型,具有 `UInt64` ID 字段
        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}

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

        /// 从集合中移除一个 NFT 并将其移到调用者。
        ///
        /// @param withdrawID: 想要提取的 NFT 的 ID。
        /// @return 从集合中拿出的 NFT 资源。
        ///
        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("缺少 NFT")

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

            return <-token
        }

        /// 将 NFT 添加到集合的字典中并将 ID 添加到 ID 数组。
        ///
        /// @param token: 要纳入集合的 NFT 资源。
        ///
        pub fun deposit(token: @NonFungibleToken.NFT) {
            let token <- token as! @QuickNFT.NFT

            let id: UInt64 = token.id

            // 将新资产添加至字典,同时移除旧资产
            let oldToken <- self.ownedNFTs[id] <- token

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

            destroy oldToken
        }

        /// 获取集合 ID 的帮助方法。
        ///
        /// @return 包含该集合中 NFTs 的 ID 数组。
        ///
        pub fun getIDs(): [UInt64] {
            return self.ownedNFTs.keys
        }

        /// 获取集合中某个 NFT 的引用,使调用者可以读取其元数据并调用其方法。
        ///
        /// @param id: 想要的 NFT 的 ID。
        /// @return 所需 NFT 资源的引用。
        ///
        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
        }

        /// 获取集合中某个 NFT 的引用,使调用者可以读取其元数据并调用其方法。
        ///
        /// @param id: 想要的 NFT 的 ID。
        /// @return 所需 NFT 资源的引用。
        ///
        pub fun borrowQuickNFT(id: UInt64): &QuickNFT.NFT? {
            if self.ownedNFTs[id] != nil {
                // 创建已授权的引用以便进行下行转换
                let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
                return ref as! &QuickNFT.NFT
            }

            return nil
        }

        /// 仅获取符合 `{MetadataViews.Resolver}` 接口的 NFT 的参考,以便调用者能够检索 NFT 实现的视图并解析它们。
        ///
        /// @param id: 想要的 NFT 的 ID。
        /// @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
        }
    }

    /// 允许任何人创建一个新的空集合。
    ///
    /// @return 新的 Collection 资源。
    ///
    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
        return <- create Collection()
    }

    /// 向接收者的集合中铸造带有新 ID 的 NFT。
    ///
    /// @param recipient:一个对将存入 NFT 的集合的能力。
    /// @param name: NFT 元数据的名称。
    /// @param description: NFT 元数据的描述。
    /// @param thumbnail: NFT 元数据的缩略图。
    ///
    pub fun mintNFT(
        recipient: &{NonFungibleToken.CollectionPublic},
        name: String,
        description: String,
        thumbnail: String,
    ) {
        let metadata: {String: AnyStruct} = {}

        // 此部分元数据将用于在特征中嵌入稀有度。
        metadata["foo"] = "bar"

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

        // 使用接收者引用将其存入 NFT 到接收者的账户。
        recipient.deposit(token: <-newNFT)

        QuickNFT.totalSupply = QuickNFT.totalSupply + 1
    }

    /// 为此合约解析元数据视图的函数。
    ///
    /// @param 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: "示例集合",
                    description: "此集合用作示例,以帮助你开发下一个 Flow NFT。",
                    externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"),
                    squareImage: media,
                    bannerImage: media,
                    socials: {
                        "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain")
                    }
                )
        }
        return nil
    }

    /// 返回所有由非同质化代币实现的元数据视图
    ///
    /// @return 定义已实现视图的类型数组。此值将
    ///         供开发者知道应传递哪些参数给 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)

        // 创建集合的公共能力链接
        self.account.link<&QuickNFT.Collection{NonFungibleToken.CollectionPublic, QuickNFT.QuickNFTCollectionPublic, MetadataViews.ResolverCollection}>(
            self.CollectionPublicPath,
            target: self.CollectionStoragePath
        )

        emit ContractInitialized()
    }
}

现在我们逐步解释智能合约。

导入:

代码导入了三个接口:NonFungibleTokenViewResolverMetadataViews。这些接口提供了创建和管理非同质化代币(NFT)在 Flow 区块链上的所需功能。

由于 Flow 团队已经在测试网上部署了这些接口,我们使用它们的地址来导入这些接口。请随意查看 非同质化代币合约页面 以查看每个网络的 NonFungibleToken 地址。

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

合约声明:

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

合约状态:

  • totalSupply: 一个变量,跟踪存在的 QuickNFT 的总数量。

  • 事件: 合约定义了多个事件,例如 ContractInitializedWithdrawDeposit,可用于记录重要的合约操作。

存储和路径:

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

NFT 资源:

NFT 资源代表一个单独的 NFT,具有各种属性,如 idnamedescriptionthumbnailmetadata。NFT 通过此资源铸造和管理。

NFT 资源中的函数:

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

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

QuickNFTCollectionPublic:

这是一个资源接口,定义了将 NFT 存入、检索和借用的相关方法。

Collection 资源:

Collection 资源代表账户所拥有的 NFT 集合。 定义了 depositwithdrawgetIDsborrowNFT 等函数,以管理集合中的 NFT。 destroy() 函数用于在不再需要时清除集合。

创建集合和铸造 NFT:

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

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

合约的元数据视图:

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

初始化:

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

创建脚本

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

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

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

GetIDsQuickNFT

GetIDsQuickNFT 脚本旨在检索与给定地址相关的 NFTs 的 IDs,利用 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("无法借用参考集合")

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

以下是代码解释:

  • 该脚本从 Flow 区块链的特定地址导入名为 MetadataViews 的接口。- 合约定义了一个叫 main 的函数,该函数接受一个 Address 作为参数。

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

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

  • 如果借用集合引用的尝试失败,它将触发一个 panic,并显示消息 "Could not borrow a reference to the collection"。

  • 假设成功借用引用,函数随后调用集合上的 getIDs() 方法来检索代表集合中 NFT IDs 的 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 和一个表示感兴趣的 NFT 的 UInt64 ID

  • main 函数内部,它检索与提供的地址关联的账户。

  • 然后,它尝试借用与该账户关联的 NFT 集合的引用。该集合预计会支持 MetadataViews.ResolverCollection 接口。如果借用尝试失败,将触发一个 panic,显示消息 "Could not borrow a reference to the collection"。

  • 成功借用集合引用后,函数调用 borrowViewResolver 获取给定 ID 的特定 NFT 的引用。

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

  • 它开始获取 NFT 的基本显示信息。如果 NFT 支持 MetadataViews.Display 视图,提取名称、描述和缩略图 URI 并将其存储在数据结构中。

  • 最后,函数填充 data 结构,包括拥有者地址、NFT ID 和其他细节,然后返回。

  • 脚本还定义了一个 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() 函数创建一个新的空集合。

    • 然后,通过 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 收件人的地址)、名称、描述和缩略图。

  • 交易中有三个主要部分:prepareexecutepost

  • prepare 阶段(在交易确认前执行),合同执行以下任务:

    • 它将当前的 NFT 总供应量存储在 mintingIDBefore 变量中,代表交易执行前的 NFT ID。

    • 它尝试借用收件人的公共 NFT 集合引用,使用收件人的地址。

    • 如果借用集合引用失败,则触发 panic,表示无法检索引用。

  • execute 阶段(在交易确认时执行),合同使用 QuickNFT.mintNFT 函数铸造新的 NFT。该函数会将新铸造的 NFT 存入收件人的集合中,并包括提供的元数据(名称、描述和缩略图)。

  • 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"]
    }
  }
}

得益于项目创建时的坞木功能,所需的所有文件和文件夹均已创建。现在,是时候将智能合约部署到测试网上并同时运行 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 应用程序进行任何处理。然而,由于我们选择的构架 ( [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 钩子检查用户是否已登录
  const { loggedIn } = useCurrentUser();

  // 返回 web 页面的 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"; // 用于用户身份验证的自定义钩子
import { useEffect, useState } from "react"; // React 钩子用于管理状态
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 IDs 的 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"; // 自定义钩子用于配置
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("");

  // 自定义钩子获取网络配置
  const { network } = useConfig();

  // 自定义钩子获取用户身份验证状态
  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("Check your setup transaction.");
  };

  // 处理铸造新 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("Check your NFT minting transaction.");

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

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

    try {
      // 查询区块链以获取用户拥有的 NFT 的 IDs
      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);
    }
  };

  // 当用户经过身份验证时,使用效果钩子获取用户的 NFT
  useEffect(() => {
    if (user && user.addr) {
      fetchNFTs();
    }
  }, [user]);

  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 Thumbnail"} />
              <p>{`${item.description} #${item.id}`}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

容器样式

Container.module.css 文件用于容器的 CSS 样式。我们想要修改此文件,以便在画廊视图中显示连接帐户拥有的 NFTs。

打开 ./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;
  /* 子项的负外边距以抵消填充 */
}

/* 为每个 NFT 项目设置样式 */
.nft {
  width: calc(20% - 20px);
  /* 根据需要调整宽度以适应布局 */
  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)。

点击 使用钱包登录,并选择你的 Flow 钱包。在使用 Lilico 的情况下,设置钱包后,你可以在创建你的 Flow 账户。在本指南中,我们使用 Blocto 来简化操作,因为它只需要电子邮件地址即可创建账户。

将钱包连接到网站后,你应该能够看到如下内容。

Homepage

首先,正如我们在智能合约开发中提到的,用户应该设置其账户才能铸造 NFTs。

点击 设置账户,然后弹出 确认交易 页面。然后,点击 批准。在交易发送到区块链后,你可以通过点击 检查你的设置交易 消息来查看交易。

Set Up Account Transaction

然后,点击 铸造 NFT 并批准交易。如果一切顺利,你的 NFT 应该显示在 你的 NFTs 部分下。

此外,你如果点击 查询总供应量,整体铸造的 NFT 数量将从区块链中获取并显示。

Minting NFT

恭喜你!你刚刚在 Flow 测试网上创建了一个 NFT 集合,并通过前端成功铸造了一个 NFT。

目前,我们已经完成了开发部分。现在,让我们看看为什么 Flow 是 NFT 项目的合适区块链。

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

重新审视 Cadence 中的所有权

希望你现在可以通过本指南前面的内容理解,Flow 中的 NFT 只是实现了非同质化代币标准的资源。Cadence 中的资源是类型对象的实例,因为 Cadence 是面向对象的语言,这意味着你可以将资源存储在另一个资源中。

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

Cadence 中可能实现的面向对象封装彻底改变了复杂链上应用程序的可能性。逻辑内部化于资源中,通过能力控件访问这些逻辑的结果是安全良好的解决方案,无法被利用。这种提供的可移植性也非常强大。如果 NFT A 内部存储了其他 NFTs(B 和 C),它被发送到另一个账户,那么接收用户可以期待 A 仍然包含 NFTs B 和 C。整个嵌套资源的图形是原子地从发送账户转移到接收者,没有改变 B 和 C 在 A 中的存储方式。在任何时候,交易都无需参考 B 或 C 来实现此目的,传播的唯一 NFT 是 A。

Flow 与 EIP-6551 的比较

在实际层面,EIP-6551 是一种框架,使 ERC-721 代币在 EVM 链上能够“存储”其他 NFT。重要的是,它是对 Solidity 的扩展(因此称为 EIP),旨在标准化 NFT 拥有其他 NFT 的方式。该提案未更改 Solidity 语言,并确保所有权关系通过 EIP-6551 框架进行管理。

Cadence 的主要区别在于所有权和封装直接内置在语法中,这是由其面向对象的语言设计导致的。这是一种许多开发者可能熟悉的编程模型。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 之间的主要区别如下:

  • EIP-6551 的实现更加复杂,并且使用起来不如直接在 Cadence 中编写你的 OO(面向对象)模型直观。
  • EIP-6551 模拟了某种对象组合的程度,以绕过 Solidity 缺乏本地支持的限制。使用 EIP-6551 时,可能会有限制可实现的类型或组合的复杂性,例如:嵌套对象图。
  • EIP-6551 仍处于提案阶段,仍在开发中。这意味着现有 NFT 项目和链的支持有限。Cadence 自 2020 年 10 月以来已在 Flow 主网运行。
  • EIP-6551 的复杂性增加了应用程序的外部环境,而这可能会导致安全问题。
  • Cadence 的设计意味着每个资源可以唯一地定义自己的安全模型,同时控件管理必要的能力。随着应用程序变得越来越复杂,这变得越来越重要。
  • Cadence 中的 NFT 始终存储在账户中。然而,它们并不与“它们拥有”的账户固有链接;EIP-6551 中需要该构造来使解决方案工作。Cadence 中的资源如果需要,可以内部化一个 AuthAccount 对象,但这个高级主题超出了本摘要的范围。
  • 嵌套资源的可移植性由语言保证。根据 EIP-6551 的设计,必须为从拥有 NFT 批量转移拥有的 NFTs 编写代码。

结论

就是这样!到目前为止,你应该清楚理解 Flow 的独特特性,例如面向资源的架构和 资源拥有资源 的概念,以及如何使用 Cadence 和 Flow CLI 创建 NFT 集合去中心化应用程序。

总之,本指南解释了 Flow 和其编程语言 Cadence 的关键特性,并帮助你在 Flow 上创建自己的 NFT 集合 dApp,充分利用 Flow 的特殊特性。凭借这些知识,你可以加入数字资产和去中心化应用程序的世界,将你的创意理念付诸于区块链空间。

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

Flow 生态系统

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

我们 ❤️ 反馈!

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

  • 原文链接: quicknode.com/guides/oth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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